/*
 Navicat Premium Data Transfer

 Source Server         : MyAlibaba
 Source Server Type    : MySQL
 Source Server Version : 50728
 Source Host           : 123.57.140.84:3306
 Source Schema         : My_Blog_db

 Target Server Type    : MySQL
 Target Server Version : 50728
 File Encoding         : 65001

 Date: 01/04/2020 12:28:51
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu`  (
  `menu_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
  `menu_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '菜单名称',
  `parent_id` bigint(20) NULL DEFAULT 0 COMMENT '父菜单ID',
  `order_num` int(4) NULL DEFAULT 0 COMMENT '显示顺序',
  `path` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '路由地址',
  `component` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '组件路径',
  `is_frame` int(1) NULL DEFAULT 1 COMMENT '是否为外链（0是 1否）',
  `menu_type` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '菜单类型（M目录 C菜单 F按钮）',
  `visible` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '菜单状态（0显示 1隐藏）',
  `perms` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限标识',
  `icon` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '#' COMMENT '菜单图标',
  `remark` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '备注',
  `create_time` timestamp(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `update_time` timestamp(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  PRIMARY KEY (`menu_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2020 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '菜单权限表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_menu
-- ----------------------------
INSERT INTO `sys_menu` VALUES (1, '系统管理', 0, 1, 'system', NULL, 1, 'M', '0', '', 'system', '系统管理目录', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_menu` VALUES (2, '内容管理', 0, 2, 'content', NULL, 1, 'M', '0', '', 'guide', '系统内容目录', '2018-03-16 11:33:00', '2020-03-30 16:02:35');
INSERT INTO `sys_menu` VALUES (100, '用户管理', 1, 1, 'user', 'system/user/index', 1, 'C', '0', 'system:user:list', 'user', '用户管理菜单', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_menu` VALUES (101, '角色管理', 1, 2, 'role', 'system/role/index', 1, 'C', '0', 'system:role:list', 'peoples', '角色管理菜单', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_menu` VALUES (102, '博客管理', 2, 1, 'blog', 'content/blog/page', 1, 'M', '0', 'content:blog:list', 'tree', '博客管理菜单', '2018-03-16 11:33:00', '2020-04-01 10:53:35');
INSERT INTO `sys_menu` VALUES (103, '标签管理', 2, 2, 'tag', 'content/tag/index', 1, 'C', '0', 'content:tag:list', 'post', '标签管理菜单', '2018-03-16 11:33:00', '2020-04-01 10:25:09');
INSERT INTO `sys_menu` VALUES (104, '类型管理', 2, 3, 'type', 'content/type/index', 1, 'C', '0', 'content:type:list', 'dict', '类型管理菜单', '2018-03-16 11:33:00', '2020-04-01 10:25:12');
INSERT INTO `sys_menu` VALUES (105, '友链管理', 2, 4, 'friend', 'content/friend/index', 1, 'C', '0', 'content:friend:list', 'wechat', '友链管理菜单', '2018-03-16 11:33:00', '2020-04-01 10:25:14');
INSERT INTO `sys_menu` VALUES (106, '评论管理', 2, 5, 'comment', 'content/comment/index', 1, 'C', '0', 'content:comment:list', 'message', '评论管理菜单', '2018-03-16 11:33:00', '2020-04-01 10:25:16');
INSERT INTO `sys_menu` VALUES (107, '日志查看', 2, 6, 'requestlog', 'content/requestlog/index', 1, 'C', '0', 'content:requestlog:list', 'log', '日志管理菜单', '2020-04-01 09:59:02', '2020-04-01 10:25:23');
INSERT INTO `sys_menu` VALUES (108, '博客查询', 102, 1, 'index', 'content/blog/index', 1, 'C', '0', 'content:blog:query', 'nested', '博客列表', '2020-03-30 19:43:00', '2020-04-01 11:06:07');
INSERT INTO `sys_menu` VALUES (109, '博客添加', 102, 2, 'add', 'content/blog/add', 1, 'C', '0', 'content:blog:add', 'form', '博客添加', '2020-03-30 19:32:48', '2020-04-01 10:25:43');
INSERT INTO `sys_menu` VALUES (110, '博客修改', 102, 3, 'edit', 'content/blog/edit', 1, 'C', '0', 'content:blog:edit', 'edit', '博客修改', '2020-03-30 15:44:28', '2020-04-01 10:25:27');
INSERT INTO `sys_menu` VALUES (111, '博客删除', 102, 4, '', NULL, 1, 'F', '0', 'content:blog:remove', '#', '', '2020-04-01 10:51:56', '2020-04-01 10:52:16');
INSERT INTO `sys_menu` VALUES (112, '博客状态', 102, 5, '', NULL, 1, 'F', '0', 'content:blog:setPub', '#', '', '2020-04-01 10:56:43', '2020-04-01 10:56:43');
INSERT INTO `sys_menu` VALUES (1001, '用户查询', 100, 1, '', '', 1, 'F', '0', 'system:user:query', '#', '', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_menu` VALUES (1002, '用户新增', 100, 2, '', '', 1, 'F', '0', 'system:user:add', '#', '', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_menu` VALUES (1003, '用户修改', 100, 3, '', '', 1, 'F', '0', 'system:user:edit', '#', '', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_menu` VALUES (1004, '用户删除', 100, 4, '', '', 1, 'F', '0', 'system:user:remove', '#', '', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_menu` VALUES (1005, '重置密码', 100, 5, '', '', 1, 'F', '0', 'system:user:resetPwd', '#', '', '2018-03-16 11:33:00', '2020-03-31 15:48:32');
INSERT INTO `sys_menu` VALUES (1008, '角色查询', 101, 1, '', '', 1, 'F', '0', 'system:role:query', '#', '', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_menu` VALUES (1009, '角色新增', 101, 2, '', '', 1, 'F', '0', 'system:role:add', '#', '', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_menu` VALUES (1010, '角色修改', 101, 3, '', '', 1, 'F', '0', 'system:role:edit', '#', '', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_menu` VALUES (1011, '角色删除', 101, 4, '', '', 1, 'F', '0', 'system:role:remove', '#', '', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_menu` VALUES (2003, '标签查询', 103, 1, '', NULL, 1, 'F', '0', 'content:tag:query', '#', '', '2020-04-01 10:29:04', '2020-04-01 10:29:20');
INSERT INTO `sys_menu` VALUES (2004, '标签新增', 103, 2, '', NULL, 1, 'F', '0', 'content:tag:add', '#', '', '2020-04-01 10:29:44', '2020-04-01 10:30:05');
INSERT INTO `sys_menu` VALUES (2005, '标签修改', 103, 3, '', NULL, 1, 'F', '0', 'content:tag:edit', '#', '', '2020-04-01 10:29:59', '2020-04-01 10:30:25');
INSERT INTO `sys_menu` VALUES (2006, '标签删除', 103, 4, '', NULL, 1, 'F', '0', 'content:tag:remove', '#', '', '2020-04-01 10:30:23', '2020-04-01 10:30:40');
INSERT INTO `sys_menu` VALUES (2007, '类型查询', 104, 1, '', NULL, 1, 'F', '0', 'content:type:query', '#', '', '2020-04-01 10:31:05', '2020-04-01 10:31:15');
INSERT INTO `sys_menu` VALUES (2008, '类型新增', 104, 2, '', NULL, 1, 'F', '0', 'content:type:add', '#', '', '2020-04-01 10:31:37', '2020-04-01 10:31:37');
INSERT INTO `sys_menu` VALUES (2009, '类型修改', 104, 3, '', NULL, 1, 'F', '0', 'content:type:edit', '#', '', '2020-04-01 10:31:53', '2020-04-01 10:31:53');
INSERT INTO `sys_menu` VALUES (2010, '类型删除', 104, 4, '', NULL, 1, 'F', '0', 'content:type:remove', '#', '', '2020-04-01 10:32:09', '2020-04-01 10:32:09');
INSERT INTO `sys_menu` VALUES (2011, '友链查询', 105, 1, '', NULL, 1, 'F', '0', 'content:friend:query', '#', '', '2020-04-01 10:32:49', '2020-04-01 10:32:58');
INSERT INTO `sys_menu` VALUES (2012, '友链新增', 105, 2, '', NULL, 1, 'F', '0', 'content:friend:add', '#', '', '2020-04-01 10:33:14', '2020-04-01 10:33:14');
INSERT INTO `sys_menu` VALUES (2013, '友链修改', 105, 3, '', NULL, 1, 'F', '0', 'content:friend:edit', '#', '', '2020-04-01 10:33:28', '2020-04-01 10:33:28');
INSERT INTO `sys_menu` VALUES (2014, '友链删除', 105, 4, '', NULL, 1, 'F', '0', 'content:friend:remove', '#', '', '2020-04-01 10:33:50', '2020-04-01 10:33:58');
INSERT INTO `sys_menu` VALUES (2015, '评论查询', 106, 1, '', NULL, 1, 'F', '0', 'content:comment:query', '#', '', '2020-04-01 10:34:43', '2020-04-01 10:34:43');
INSERT INTO `sys_menu` VALUES (2016, '评论删除（逻辑删除）', 106, 2, '', NULL, 1, 'F', '0', 'content:comment:delete', '#', '', '2020-04-01 10:35:21', '2020-04-01 10:35:21');
INSERT INTO `sys_menu` VALUES (2017, '日志查询', 107, 1, '', NULL, 1, 'F', '0', 'content:requestlog:query', '#', '', '2020-04-01 10:36:00', '2020-04-01 10:36:16');
INSERT INTO `sys_menu` VALUES (2018, '日志删除', 107, 2, '', NULL, 1, 'F', '0', 'content:requestlog:remove', '#', '', '2020-04-01 10:36:41', '2020-04-01 10:38:21');

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role`  (
  `role_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
  `role_name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色名称',
  `role_key` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色权限字符串',
  `role_sort` int(4) NOT NULL COMMENT '显示顺序',
  `data_scope` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '1' COMMENT '数据范围（1：全部数据权限 2：自定数据权限 3：本部门数据权限 4：本部门及以下数据权限）',
  `status` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色状态（0正常 1停用）',
  `del_flag` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '删除标志（0代表存在 2代表删除）',
  `create_by` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '创建者',
  `update_by` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '更新者',
  `remark` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
  `create_time` timestamp(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `update_time` timestamp(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  PRIMARY KEY (`role_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 105 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色信息表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, '管理员', 'admin', 1, '1', '0', '0', 'admin', 'ry', '管理员', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_role` VALUES (2, '普通角色', 'common', 2, '2', '0', '0', 'admin', 'ry', '普通角色', '2018-03-16 11:33:00', '2018-03-16 11:33:00');
INSERT INTO `sys_role` VALUES (102, '访客', 'test', 3, '1', '0', '0', '', '', 'test', '2020-03-29 22:01:10', '2020-03-29 22:01:10');

-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu`  (
  `role_id` bigint(20) NOT NULL COMMENT '角色ID',
  `menu_id` bigint(20) NOT NULL COMMENT '菜单ID',
  PRIMARY KEY (`role_id`, `menu_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色和菜单关联表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_role_menu
-- ----------------------------
INSERT INTO `sys_role_menu` VALUES (2, 1);
INSERT INTO `sys_role_menu` VALUES (2, 2);
INSERT INTO `sys_role_menu` VALUES (2, 3);
INSERT INTO `sys_role_menu` VALUES (2, 4);
INSERT INTO `sys_role_menu` VALUES (2, 100);
INSERT INTO `sys_role_menu` VALUES (2, 101);
INSERT INTO `sys_role_menu` VALUES (2, 102);
INSERT INTO `sys_role_menu` VALUES (2, 103);
INSERT INTO `sys_role_menu` VALUES (2, 104);
INSERT INTO `sys_role_menu` VALUES (2, 105);
INSERT INTO `sys_role_menu` VALUES (2, 106);
INSERT INTO `sys_role_menu` VALUES (2, 107);
INSERT INTO `sys_role_menu` VALUES (2, 108);
INSERT INTO `sys_role_menu` VALUES (2, 109);
INSERT INTO `sys_role_menu` VALUES (2, 110);
INSERT INTO `sys_role_menu` VALUES (2, 111);
INSERT INTO `sys_role_menu` VALUES (2, 112);
INSERT INTO `sys_role_menu` VALUES (2, 113);
INSERT INTO `sys_role_menu` VALUES (2, 114);
INSERT INTO `sys_role_menu` VALUES (2, 115);
INSERT INTO `sys_role_menu` VALUES (2, 500);
INSERT INTO `sys_role_menu` VALUES (2, 501);
INSERT INTO `sys_role_menu` VALUES (2, 1001);
INSERT INTO `sys_role_menu` VALUES (2, 1002);
INSERT INTO `sys_role_menu` VALUES (2, 1003);
INSERT INTO `sys_role_menu` VALUES (2, 1004);
INSERT INTO `sys_role_menu` VALUES (2, 1005);
INSERT INTO `sys_role_menu` VALUES (2, 1006);
INSERT INTO `sys_role_menu` VALUES (2, 1007);
INSERT INTO `sys_role_menu` VALUES (2, 1008);
INSERT INTO `sys_role_menu` VALUES (2, 1009);
INSERT INTO `sys_role_menu` VALUES (2, 1010);
INSERT INTO `sys_role_menu` VALUES (2, 1011);
INSERT INTO `sys_role_menu` VALUES (2, 1012);
INSERT INTO `sys_role_menu` VALUES (2, 1013);
INSERT INTO `sys_role_menu` VALUES (2, 1014);
INSERT INTO `sys_role_menu` VALUES (2, 1015);
INSERT INTO `sys_role_menu` VALUES (2, 1016);
INSERT INTO `sys_role_menu` VALUES (2, 1017);
INSERT INTO `sys_role_menu` VALUES (2, 1018);
INSERT INTO `sys_role_menu` VALUES (2, 1019);
INSERT INTO `sys_role_menu` VALUES (2, 1020);
INSERT INTO `sys_role_menu` VALUES (2, 1021);
INSERT INTO `sys_role_menu` VALUES (2, 1022);
INSERT INTO `sys_role_menu` VALUES (2, 1023);
INSERT INTO `sys_role_menu` VALUES (2, 1024);
INSERT INTO `sys_role_menu` VALUES (2, 1025);
INSERT INTO `sys_role_menu` VALUES (2, 1026);
INSERT INTO `sys_role_menu` VALUES (2, 1027);
INSERT INTO `sys_role_menu` VALUES (2, 1028);
INSERT INTO `sys_role_menu` VALUES (2, 1029);
INSERT INTO `sys_role_menu` VALUES (2, 1030);
INSERT INTO `sys_role_menu` VALUES (2, 1031);
INSERT INTO `sys_role_menu` VALUES (2, 1032);
INSERT INTO `sys_role_menu` VALUES (2, 1033);
INSERT INTO `sys_role_menu` VALUES (2, 1034);
INSERT INTO `sys_role_menu` VALUES (2, 1035);
INSERT INTO `sys_role_menu` VALUES (2, 1036);
INSERT INTO `sys_role_menu` VALUES (2, 1037);
INSERT INTO `sys_role_menu` VALUES (2, 1038);
INSERT INTO `sys_role_menu` VALUES (2, 1039);
INSERT INTO `sys_role_menu` VALUES (2, 1040);
INSERT INTO `sys_role_menu` VALUES (2, 1041);
INSERT INTO `sys_role_menu` VALUES (2, 1042);
INSERT INTO `sys_role_menu` VALUES (2, 1043);
INSERT INTO `sys_role_menu` VALUES (2, 1044);
INSERT INTO `sys_role_menu` VALUES (2, 1045);
INSERT INTO `sys_role_menu` VALUES (2, 1046);
INSERT INTO `sys_role_menu` VALUES (2, 1047);
INSERT INTO `sys_role_menu` VALUES (2, 1048);
INSERT INTO `sys_role_menu` VALUES (2, 1049);
INSERT INTO `sys_role_menu` VALUES (2, 1050);
INSERT INTO `sys_role_menu` VALUES (2, 1051);
INSERT INTO `sys_role_menu` VALUES (2, 1052);
INSERT INTO `sys_role_menu` VALUES (2, 1053);
INSERT INTO `sys_role_menu` VALUES (2, 1054);
INSERT INTO `sys_role_menu` VALUES (2, 1055);
INSERT INTO `sys_role_menu` VALUES (2, 1056);
INSERT INTO `sys_role_menu` VALUES (2, 1057);
INSERT INTO `sys_role_menu` VALUES (2, 1058);
INSERT INTO `sys_role_menu` VALUES (2, 1059);
INSERT INTO `sys_role_menu` VALUES (2, 1060);
INSERT INTO `sys_role_menu` VALUES (102, 2);
INSERT INTO `sys_role_menu` VALUES (102, 102);
INSERT INTO `sys_role_menu` VALUES (102, 103);
INSERT INTO `sys_role_menu` VALUES (102, 104);
INSERT INTO `sys_role_menu` VALUES (102, 105);
INSERT INTO `sys_role_menu` VALUES (102, 106);
INSERT INTO `sys_role_menu` VALUES (102, 107);
INSERT INTO `sys_role_menu` VALUES (102, 108);
INSERT INTO `sys_role_menu` VALUES (102, 109);
INSERT INTO `sys_role_menu` VALUES (102, 2003);
INSERT INTO `sys_role_menu` VALUES (102, 2007);
INSERT INTO `sys_role_menu` VALUES (102, 2011);
INSERT INTO `sys_role_menu` VALUES (102, 2015);
INSERT INTO `sys_role_menu` VALUES (102, 2017);

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `us_id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '姓名',
  `username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '账号',
  `password` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码',
  `email` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
  `about` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '简介',
  `location` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '地区',
  `wechat` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '微信',
  `qq` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'QQ',
  `avatar` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '头像',
  `role_id` int(11) NOT NULL COMMENT '权限号',
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  PRIMARY KEY (`us_id`) USING BTREE,
  INDEX `role_id`(`role_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, '低调做个路人', 'weipeng', '775736156', '1143038749@qq.com', '他的希望和信心从不消失，如今正像微风渐起那么重新旺盛起来。—《老人与海》', '北京朝阳区', '18531159505', '1143038749', 'http://sjpeng.top/140b9dad72e74e8692dd94df5dd5a561.jpeg', 1, '2019-12-06 14:33:53', '2020-03-31 15:45:58');
INSERT INTO `sys_user` VALUES (2, '游客01', 'youke', '789456123', '123456@163.com', NULL, '北京', '17633206320', '666666', '999', 2, '2020-01-06 14:33:53', '2020-03-29 13:16:33');
INSERT INTO `sys_user` VALUES (6, 'test01', 'test01', 't1t1t1t1t1', 't11@qq.co', '11', '11', 't11', 't11', 't11', 2, '2020-03-29 13:30:47', '2020-03-29 13:47:24');
INSERT INTO `sys_user` VALUES (8, 'test02', 'test02', '111222', 'youke@qq.co', NULL, NULL, NULL, NULL, 'http://sjpeng.top/586273feb2d44a6ba981164a1bca3502.jpeg', 102, '2020-03-29 22:20:16', '2020-03-30 19:23:15');

-- ----------------------------
-- Table structure for t_blog
-- ----------------------------
DROP TABLE IF EXISTS `t_blog`;
CREATE TABLE `t_blog`  (
  `bl_id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '标题',
  `content` text CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '内容',
  `outline` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '简介',
  `background_image` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '背景图片',
  `recommend` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否推荐',
  `commentabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否可以评论',
  `published` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否发布',
  `views` int(11) NOT NULL DEFAULT 0 COMMENT '访问量',
  `ty_id` int(11) NOT NULL COMMENT '类型id',
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  PRIMARY KEY (`bl_id`) USING BTREE,
  INDEX `ty_id`(`ty_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 87 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_blog
-- ----------------------------
INSERT INTO `t_blog` VALUES (1, 'CAS', '<h5>CAS全称Compare-And-Swap,他是一条cpu并发原语.</h5>\n<h5>它的功能是判断内存某个位置是否是预期值,是的话更改为更新值,是一条原子操作.</h5>\n<h5>CAS并发原语在java语言中是sun.misc.Unsafe 类中的各个方法. 调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令,这是一种完全依赖硬件的功能,通过它实现了原子操作.</h5>\n<h5>由于CAS是一种系统原语,原语属于操作系统范畴,由若干条指令组成,用于完成某一功能的过程.</h5>\n<h5>并且原语的执行过程必须是连续的.在执行过程中无法被打断,所以不会造成数据不一致问题.</h5>\n<h4 id=\"UnSafe-类\">UnSafe 类</h4>\n<pre class=\"language-java\"><code>public class AtomicInteger extends Number implements java.io.Serializable {\n    private static final long serialVersionUID = 6214790243416807050L;\n\n    // setup to use Unsafe.compareAndSwapInt for updates\n    private static final Unsafe unsafe = Unsafe.getUnsafe();\n    private static final long valueOffset;\n\n    static {\n        try {\n            // 获取下面 value 的地址偏移量\n            valueOffset = unsafe.objectFieldOffset\n                (AtomicInteger.class.getDeclaredField(\"value\"));\n        } catch (Exception ex) { throw new Error(ex); }\n    }\n\n    private volatile int value;\n	// ...\n}</code></pre>\n<ul>\n<li>Unsafe 是 CAS 的核心类，由于 Java 方法无法直接访问底层系统，而需要通过本地（native）方法来访问， Unsafe 类相当一个后门，基于该类可以直接操作特定内存的数据。Unsafe 类存在于 sun.misc 包中，其内部方法操作可以像 C 指针一样直接操作内存，因为 Java 中 CAS 操作执行依赖于 Unsafe 类。</li>\n<li>变量 vauleOffset，表示该变量值在内存中的偏移量，因为 Unsafe 就是根据内存偏移量来获取数据的。</li>\n<li>变量 value 用 volatile 修饰，保证了多线程之间的内存可见性。</li>\n</ul>\n<p><strong>getAndAddInt 方法</strong></p>\n<pre class=\"language-java\"><code>// unsafe.getAndAddInt\npublic final int getAndAddInt(Object obj, long valueOffset, long expected, int val) {\n    int temp;\n    do {\n        temp = this.getIntVolatile(obj, valueOffset);  // 获取快照值\n    } while (!this.compareAndSwap(obj, valueOffset, temp, temp + val));  // 如果此时 temp 没有被修改，就能退出循环，否则重新获取\n    return temp;\n}</code></pre>\n<h3 id=\"CAS-的缺点？\">CAS 的缺点</h3>\n<ul>\n<li>循环时间长开销很大\n<ul>\n<li>如果 CAS 失败，会一直尝试，如果 CAS 长时间一直不成功，可能会给 CPU 带来很大的开销（比如线程数很多，每次比较都是失败，就会一直循环），所以希望是线程数比较小的场景。</li>\n</ul>\n</li>\n<li>只能保证一个共享变量的原子操作\n<ul>\n<li>对于多个共享变量操作时，循环 CAS 就无法保证操作的原子性。</li>\n</ul>\n</li>\n<li>引出 ABA 问题</li>\n</ul>\n<h5 style=\"padding-left: 40px;\">ABA 问题是怎么产生的？当有一个值从 A 改为 B 又改为 A，这就是 ABA 问题。<br /><br />解决方案：时间戳原子引用</h5>', 'CAS全称Compare-And-Swap,他是一条cpu并发原语.', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 1, '2019-11-26 14:00:00', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (4, '反射中Class.forName和classloader区别', '<p>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;在java中Class.forName()和ClassLoader都可以对类进行加载。ClassLoader就是遵循<strong>双亲委派模型</strong>最终调用启动类加载器的类加载器，实现的功能是&ldquo;通过一个类的全限定名来获取描述此类的二进制字节流&rdquo;，获取到二进制流后放到JVM中。Class.forName()方法实际上也是调用的CLassLoader来实现的。</p>\n<p>&nbsp; &nbsp;&nbsp;&nbsp; Class.forName(String className)；这个方法的源码是</p>\n<pre class=\"language-java\"><code> @CallerSensitive\n    public static Class&lt;?&gt; forName(String className)\n                throws ClassNotFoundException {\n        Class&lt;?&gt; caller = Reflection.getCallerClass();\n        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);\n    }</code></pre>\n<p>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;最后调用的方法是forName0这个方法，在这个forName0方法中的第二个参数被默认设置为了true，这个参数代表是否对加载的类进行初始化，设置为true时会类进行初始化，代表会执行类中的静态代码块，以及对静态变量的赋值等操作。</p>\n<p>也可以调用Class.forName(String name, boolean initialize,ClassLoader loader)方法来手动选择在加载类的时候是否要对类进行初始化。Class.forName(String name, boolean initialize,ClassLoader loader)的源码如下：</p>\n<pre class=\"language-java\"><code> @CallerSensitive\n    public static Class&lt;?&gt; forName(String name, boolean initialize,\n                                   ClassLoader loader)\n        throws ClassNotFoundException\n    {\n        Class&lt;?&gt; caller = null;\n        SecurityManager sm = System.getSecurityManager();\n        if (sm != null) {\n            // Reflective call to get caller class is only needed if a security manager\n            // is present.  Avoid the overhead of making this call otherwise.\n            caller = Reflection.getCallerClass();\n            if (sun.misc.VM.isSystemDomainLoader(loader)) {\n                ClassLoader ccl = ClassLoader.getClassLoader(caller);\n                if (!sun.misc.VM.isSystemDomainLoader(ccl)) {\n                    sm.checkPermission(\n                        SecurityConstants.GET_CLASSLOADER_PERMISSION);\n                }\n            }\n        }\n        return forName0(name, initialize, loader, caller);\n    }</code></pre>\n<p>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;源码中的注释描述中，其中对参数initialize的描述是：if {@code true} the class will be initialized.意思就是说：如果参数为true，则加载的类将会被初始化。</p>\n<hr />\n<h1 class=\"title-article\">&nbsp;&nbsp;&nbsp;类加载过程</h1>\n<p>&nbsp;&nbsp; &nbsp;&nbsp; <strong>1：加载</strong>&nbsp;&nbsp;&nbsp;Jvm把class文件字节码加载到内存中，并将这些静态数据装换成运行时数据区中方法区的类型数据，在运行时数据区堆中生成一个代表这个类</p>\n<p><strong>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;2：链接</strong>：执行下面的校验、准备和解析步骤，其中解析步骤是可选的</p>\n<p>　　　　a：校验：检查加载的class文件的正确性和安全性</p>\n<p>　　　　b：准备：为类变量分配存储空间并设置类变量初始值，类变量随类型信息存放在方法区中,生命周期很长，使用不当和容易造成内存泄漏。</p>\n<p><em>　　　　*</em><em><strong>释</strong>：类变量就是</em><em>static</em><em>变量；初始值指的是类变量类型的默认值而不是实际要赋的值</em></p>\n<p>　　　　c：解析：jvm将常量池内的符号引用转换为直接引用</p>\n<p><strong>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;3：初始化</strong>：执行类变量赋值和静态代码块</p>\n<hr />\n<p>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;Class.forName得到的class是已经初始化完成的</p>\n<p>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;Classloder.loaderClass得到的class是还没有链接的</p>\n<hr />\n<p>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;有些情况是只需要知道这个类的存在而不需要初始化的情况使用Classloder.loaderClass，而有些时候又必须执行初始化就选择Class.forName</p>\n<p>　　例如：数据库驱动加载就是使用Class.froName(&ldquo;com.mysql.jdbc.Driver&rdquo;),</p>\n<hr />\n<p><a href=\"https://www.cnblogs.com/jimoer/p/9185662.html\" target=\"_blank\" rel=\"noopener\">原文：https://www.cnblogs.com/jimoer/p/9185662.html</a></p>', '为什么要把ClassLoader.loadClass(String name)和Class.forName(String name)进行比较呢，因为他们都能在运行时对任意一个类，都能够知道该类的所有属性和方法；对于任意一个对象，都能够调用它的任意方法和属性。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 1, 1, 1, 1, 5, '2019-11-26 14:00:00', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (5, '多线程中的虚假唤醒', '<div>\n<h1>什么是虚假唤醒？</h1>\n</div>\n<div>一般而言线程调用wait()方法后，需要其他线程调用notify,notifyAll方法后，线程才会从wait方法中返回， 而虚假唤醒(spurious wakeup)是指线程通过其他方式，从wait方法中返回。</div>\n<div>&nbsp;</div>\n<div>假设有两个消费者线程，两个生产者线程。</div>\n<div>先上错误代码，是用if的情况：</div>\n<div>\n<pre class=\"language-java\"><code>public class AirCondition {\n    private int num=0;\n\n    private synchronized void production() throws InterruptedException {\n        //1.判断\n        if(num&gt;=1){\n            this.wait();\n        }\n        //2.执行\n        this.num++;\n        System.out.println(Thread.currentThread().getName()+\"生产1个此时资源量：\"+num);\n        //3.通知\n        notifyAll();\n\n    }\n    private synchronized void consume() throws InterruptedException {\n        if(num&lt;=0){\n            this.wait();\n        }\n        this.num--;\n        System.out.println(Thread.currentThread().getName()+\"消费1个此时资源量：\"+num);\n        notifyAll();\n    }\n    \n    public static void main(String[] args) {\n        AirCondition airCondition=new AirCondition();\n        new Thread(()-&gt;{\n            for (int i=0;i&lt;10;i++){\n                try {\n                    airCondition.consume();\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n            }\n\n        },\"消费者1\").start();\n\n        new Thread(()-&gt;{\n            for (int i=0;i&lt;10;i++){\n                try {\n                    airCondition.consume();\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n            }\n\n        },\"消费者2\").start();\n\n        new Thread(()-&gt;{\n            for (int i=0;i&lt;10;i++){\n                try {\n                    airCondition.production();\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n            }\n\n        },\"生产者1\").start();\n\n        new Thread(()-&gt;{\n            for (int i=0;i&lt;10;i++){\n                try {\n                    airCondition.production();\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n            }\n\n        },\"生产者2\").start();\n    }\n\n}</code></pre>\n<pre class=\"language-java\"><code>生产者1生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n消费者1消费1个此时资源量：-1\n生产者2生产1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n消费者2消费1个此时资源量：-1\n生产者1生产1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n消费者1消费1个此时资源量：-1\n生产者2生产1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n消费者2消费1个此时资源量：-1\n生产者1生产1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n消费者1消费1个此时资源量：-1\n生产者2生产1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n消费者2消费1个此时资源量：-1\n生产者1生产1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n消费者1消费1个此时资源量：-1\n生产者2生产1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n消费者2消费1个此时资源量：-1\n生产者1生产1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n消费者1消费1个此时资源量：-1\n生产者2生产1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n消费者2消费1个此时资源量：-1\n生产者1生产1个此时资源量：0</code></pre>\n<p>发现执行时资源量出现了-1的情况原因：</p>\n</div>\n<ol>\n<li>消费者1号线程抢到锁，进入同步代码块，发现没有资源，消费者1进入wait等待状态，外面三个线程竞争锁。</li>\n<li>消费者2号线程抢到锁，进入同步代码块，发现没有资源，消费者2进入wait等待状态，外面两个线程竞争锁。（此时消费者1,2都处于等待状态）</li>\n<li>生产者1号线程抢到锁，进入同步代码块，发现没有资源，执行生产资源，然后notifyAll。（此时消费者1,2都被唤醒）</li>\n<li>消费者1从wait往后执行，不在进行if判断 消费了1个资源，资源剩余成为0。</li>\n<li>消费者2从wait往后执行，不在进行if判断 消费了1个资源，资源剩余成为-1。（bug产生）</li>\n</ol>\n<div>解决的办法是条件判断通过</div>\n<div><span style=\"color: #ff0000;\">while(条件){</span></div>\n<div><span style=\"color: #ff0000;\">&nbsp; this.wait();</span></div>\n<div><span style=\"color: #ff0000;\">}</span></div>\n<div>来解决</div>\n<div>使用while后的结果：</div>\n<div>\n<pre class=\"language-java\"><code>生产者1生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n生产者1生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者1消费1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者2消费1个此时资源量：0\n生产者2生产1个此时资源量：1\n消费者1消费1个此时资源量：0</code></pre>\n</div>\n<div>此时能够正常运行了。</div>\n<div>&nbsp;</div>\n<div>贴一下正确的代码：\n<pre class=\"language-java\"><code>public class AirCondition {\n    private int num=0;\n\n    private synchronized void production() throws InterruptedException {\n        //1.判断\n        while(num&gt;=1){\n            this.wait();\n        }\n        //2.执行\n        this.num++;\n        System.out.println(Thread.currentThread().getName()+\"生产1个此时资源量：\"+num);\n        //3.通知\n        notifyAll();\n\n    }\n    private synchronized void consume() throws InterruptedException {\n        while(num&lt;=0){\n            this.wait();\n        }\n        this.num--;\n        System.out.println(Thread.currentThread().getName()+\"消费1个此时资源量：\"+num);\n        notifyAll();\n    }\n\n    public static void main(String[] args) {\n        AirCondition airCondition=new AirCondition();\n        new Thread(()-&gt;{\n            for (int i=0;i&lt;10;i++){\n                try {\n                    airCondition.consume();\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n            }\n\n        },\"消费者1\").start();\n\n        new Thread(()-&gt;{\n            for (int i=0;i&lt;10;i++){\n                try {\n                    airCondition.consume();\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n            }\n\n        },\"消费者2\").start();\n\n        new Thread(()-&gt;{\n            for (int i=0;i&lt;10;i++){\n                try {\n                    airCondition.production();\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n            }\n\n        },\"生产者1\").start();\n\n        new Thread(()-&gt;{\n            for (int i=0;i&lt;10;i++){\n                try {\n                    airCondition.production();\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n            }\n\n        },\"生产者2\").start();\n    }\n\n}\n​</code></pre>\n</div>\n<h1>使用while()判断的原因</h1>\n<div><span style=\"color: #ff0000;\">while即使在条件成立的时候也还会再执行一次，而if判断条件成立了，就直接向下执行了</span></div>\n<div>&nbsp;</div>\n<div>\n<div>&nbsp;</div>\n<div>wait方法可以分为三个操作：</div>\n<div>（1）释放锁并阻塞</div>\n<div>（2）等待条件cond发生</div>\n<div>（3）获取通知后，竞争获取锁</div>\n</div>\n<div>在多核处理器下，pthread_cond_signal可能会激活多于一个线程（阻塞在条件变量上的线程）。结果就是，当一个线程调用pthread_cond_signal()后，多个调用pthread_cond_wait()或pthread_cond_timedwait()的线程返回。这种效应就称为&ldquo;虚假唤醒&rdquo;。</div>', '多线程使用while()判断wait的原因', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 1, 1, 1, 1, 1, '2019-11-26 14:00:00', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (6, 'JMM（Java 内存模型）', '<h4 id=\"基本概念\">基本概念</h4>\n<ul>\n<li>JMM 本身是一种抽象的概念并不是真实存在，它描述的是一组规定或则规范，通过这组规范定义了程序中的访问方式。</li>\n<li>JMM 同步规定\n<ul>\n<li>线程解锁前，必须把共享变量的值刷新回主内存</li>\n<li>线程加锁前，必须读取主内存的最新值到自己的工作内存</li>\n<li>加锁解锁是同一把锁</li>\n</ul>\n</li>\n<li>由于 JVM 运行程序的实体是线程，而每个线程创建时 JVM 都会为其创建一个工作内存，工作内存是每个线程的私有数据区域，而 Java 内存模型中规定所有变量的储存在主内存，主内存是共享内存区域，所有的线程都可以访问，但线程对变量的操作（读取赋值等）必须都工作内存进行看。</li>\n<li>首先要将变量从主内存拷贝的自己的工作内存空间，然后对变量进行操作，操作完成后再将变量写回主内存，不能直接操作主内存中的变量，工作内存中存储着主内存中的变量副本拷贝，前面说过，工作内存是每个线程的私有数据区域，因此不同的线程间无法访问对方的工作内存，线程间的通信(传值)必须通过主内存来完成。</li>\n</ul>\n<p>内存模型图</p>\n<p><img src=\"http://blog.cuzz.site/2019/04/16/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20190416211412.png\" alt=\"\" /></p>', 'JMM 本身是一种抽象的概念并不是真实存在，它描述的是一组规定或则规范，通过这组规范定义了程序中的访问方式。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 1, '2019-11-26 14:00:00', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (7, '对volatile的理解', '<div><span style=\"color: #ff0000;\">volatile:1 保证可见性 2 禁止指令重排 3 不保证原子性</span></div>\n<div>&nbsp;</div>\n<h5>以多核CPU为例（两核），我们知道CPU的速度比内存要快得多，为了弥补这个性能差异，CPU内核都会有自己的高速缓存区，当内核运行的线程执行一段代码时，首先将这段代码的指令集进行缓存行填充到高速缓存，如果非volatil变量当CPU执行修改了此变量之后，会将修改后的值回写到高速缓存，然后再刷新到内存中。如果在刷新会内存之前，由于是共享变量，那么CORE2中的线程执行的代码也用到了这个变量，这是变量的值依然是旧的。volatile关键字就会解决这个问题的，如何解决呢，首先被volatile关键字修饰的共享变量在转换成汇编语言时，会加上一个以lock为前缀的指令，当CPU发现这个指令时，立即做两件事：</h5>\n<h5>1.将当前内核高速缓存行的数据立刻回写到内存；</h5>\n<h5>2.使在其他内核里缓存了该内存地址的数据无效。</h5>\n<h5>第一步很好理解，第二步如何做到呢？</h5>\n<h5>MESI协议：在早期的CPU中，是通过在总线加LOCK#锁的方式实现的，但这种方式开销太大，所以Intel开发了缓存一致性协议，也就是MESI协议，该解决缓存一致性的思路是：当CPU写数据时，如果发现操作的变量是共享变量，即在其他CPU中也存在该变量的副本，那么他会发出信号通知其他CPU将该变量的缓存行设置为无效状态。当其他CPU使用这个变量时，首先会去嗅探是否有对该变量更改的信号，当发现这个变量的缓存行已经无效时，会从新从内存中读取这个变量。</h5>\n<h5>并发编程的三大概念</h5>\n<h5><strong>可见性</strong></h5>\n<h5>可见性是一种复杂的属性，因为可见性中的错误总是会违背我们的直觉。通常，我们无法确保执行读操作的线程能适时看到其他线程写入的值，有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性，必须使用同步机制。</h5>\n<h5>可见性，是指线程之间的可见性，一个线程修改的状态对另一个线程是可见的。也就是线程修改的结果。</h5>\n<h5>另一个线程马上就能看到。比如：用volattitle修饰的变量，就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序，即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题，volatile只能让他修饰内容具有可见性，但不能保证他具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性，但是a++ 依然是一个非原子操作，也就是这个操作同样存在线程安全问题。</h5>\n<h5>而普通的共享变量不能保证可见性，因为普通共享变量被修改之后，什么时候被写入主存是不确定的，当其他线程去读取时，此时内存中可能还是原来的旧值，因此无法保证可见性。</h5>\n<h5>在 Java 中通过synchronized和Lock也能够保证可见性，synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码，并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。</h5>\n<h5><strong>原子性</strong></h5>\n<h5>原子是世界上的最小单位，具有不可分割性。原子性：即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断，要么就都不执行。在Java中，对基本数据类型的变量的读取和赋值操作是原子性操作，即这些操作是不可被中断的，要么执行，要么不执行。</h5>\n<h5>比如 a=0;(a非long和double类型) 这个操作是不可分割的，那么我们说这个操作时原子操作。再比如：a++; 这个操作实际是a = a + 1;是可分割的，所以他不是一个原子操作。非原子操作都会存在线程安全问题，需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作，那么我们称它具有原子性。java的concurrent包下提供了一些原子类，我们可以通过阅读API来了解这些原子类的用法。比如：AtomicInteger、AtomicLong、AtomicReference等。</h5>\n<h5>Java内存模型只保证了基本读取和赋值是原子性操作，如果要实现更大范围操作的原子性，可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块，那么自然就不存在原子性问题了，从而保证了原子性。</h5>\n<h5><strong>有序性</strong></h5>\n<h5>有序性就是程序执行的顺序按照代码的先后顺序执行。</h5>\n<h5>什么是指令重排序，一般来说，处理器为了提高程序运行效率，可能会对输入代码进行优化，它不保证程序中各个语句的执行先后顺序同代码中的顺序一致，但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排序不会影响单个线程的执行，但是会影响到线程并发执行的正确性。也就是说，要想并发程序正确地执行，必须要保证原子性、可见性以及有序性。只要有一个没有被保证，就有可能会导致程序运行不正确。</h5>\n<h5>在Java内存模型中，允许编译器和处理器对指令进行重排序，但是重排序过程不会影响到单线程程序的执行，却会影响到多线程并发执行的正确性。</h5>\n<h5>在Java里面，可以通过volatile关键字来保证一定的&ldquo;有序性&rdquo;。另外可以通过synchronized和Lock来保证有序性，很显然，synchronized和Lock保证每个时刻是有一个线程执行同步代码，相当于是让线程顺序执行同步代码，自然就保证了有序性。</h5>\n<h5>另外，Java内存模型具备一些先天的&ldquo;有序性&rdquo;，即不需要通过任何手段就能够得到保证的有序性，这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来，那么它们就不能保证它们的有序性，虚拟机可以随意地对它们进行重排序</h5>\n<div><hr /></div>\n<div>\n<h4 id=\"禁止指令排序\">禁止指令排序</h4>\n<p>volatile 实现禁止指令重排序的优化，从而避免了多线程环境下程序出现乱序的现象</p>\n<p>先了解一个概念，内存屏障（Memory Barrier）又称内存栅栏，是一个 CPU 指令，他的作用有两个：</p>\n<ul>\n<li>保证特定操作的执行顺序</li>\n<li>保证某些变量的内存可见性（利用该特性实现 volatile 的内存可见性）</li>\n</ul>\n<p>由于编译器个处理器都能执行指令重排序优化，如果在指令间插入一条 Memory Barrier 则会告诉编译器和 CPU，不管什么指令都不能个这条 Memory Barrier 指令重排序，也就是说通过插入内存屏障禁止在内存屏障前后执行重排序优化。内存屏障另一个作用是强制刷出各种 CPU 缓存数据，因此任何 CPU 上的线程都能读取到这些数据的最新版本。</p>\n<p>下面是保守策略下，volatile写插入内存屏障后生成的指令序列示意图：</p>\n<p><img src=\"http://blog.cuzz.site/2019/04/16/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/0e75180bf35c40e2921493d0bf6bd684_th.png\" alt=\"\" /></p>\n<p>下面是在保守策略下，volatile读插入内存屏障后生成的指令序列示意图：</p>\n<p><img src=\"http://blog.cuzz.site/2019/04/16/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/21ebc7e8190c4966948c4ef4424088be_th.png\" alt=\"\" /></p>\n<h4 id=\"线程安全性保证\">线程安全性保证</h4>\n<ul>\n<li>工作内存与主内存同步延迟现象导致可见性问题\n<ul>\n<li>可以使用 synchronzied 或 volatile 关键字解决，它们可以使用一个线程修改后的变量立即对其他线程可见</li>\n</ul>\n</li>\n<li>对于指令重排导致可见性问题和有序性问题\n<ul>\n<li>可以利用 volatile 关键字解决，因为 volatile 的另一个作用就是禁止指令重排序优化</li>\n</ul>\n</li>\n</ul>\n</div>\n<div><hr />\n<h1 class=\"_1RuRku\">双重校验单例模式的经典实现，这里使用volatile防止指令重排</h1>\n<pre class=\"line-numbers  language-java\"><code>class Singleton{\n    private volatile static Singleton singleton;   \n    public static Singleton getInstance(){       \n\n        if(singleton == null){                      // 语句1\n            synchronized(Singleton.class){          // 语句2\n                if(singleton == null){              // 语句3\n                    singleton = new Singleton();    // 语句4\n                }\n            }\n        } \n        \n        return singleton;           \n    }\n}</code></pre>\n<pre class=\"line-numbers  language-tsx\"><code>new 一个对象实际是4个步骤：\na. 看class对象是否加载，如果没有就先加载class对象，\nb. 分配内存空间，初始化实例。\nc. 调用构造函数。\nd. 返回地址给引用。</code></pre>\n<div>\n<div>\n<h6>不加volatile会出现什么问题</h6>\n<ul>\n<li>两个线程A,B B执行到了语句4，A执行到了语句1</li>\n<li>B因为指令重排，c，d被颠倒了，恰好d执行完了,c还没执行的时候B被挂起了。</li>\n<li>此时A运行到了语句1， 发现<code>singleton</code>不等于null,于是将还没构造完成的singleton对象返回给了上层调用。</li>\n</ul>\n</div>\n</div>\n</div>', '1 保证可见性 2 禁止指令重排 3 不保证原子性', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 1, '2019-11-26 14:00:00', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (8, 'JAVA垃圾回收-可达性分析算法', '<p>在java中是通过引用来和对象进行关联的，也就是说如果要操作对象，必须通过引用来进行。那么很显然一个简单的办法就是通过引用计数来判断一个对象是否可以被回收。不失一般性，如果一个对象没有任何引用与之关联，则说明该对象基本不太可能在其他地方被使用到，那么这个对象就成为可被回收的对象了。这种方式成为引用计数法。</p>\n<p>这种方式的特点是实现简单，而且效率较高，但是它无法解决循环引用的问题，因此在Java中并没有采用这种方式（Python采用的是引用计数法）。看下面这段代码：</p>\n<pre class=\"language-java\"><code>public class Main {\n    public static void main(String[] args) {\n        MyObject object1 = new MyObject();\n        MyObject object2 = new MyObject();\n \n        object1.object = object2;\n        object2.object = object1;\n \n        object1 = null;\n        object2 = null;\n    }\n}\n \nclass MyObject{\n    public Object object = null;\n}</code></pre>\n<p><span style=\"color: #ff0000;\">最后面两句将object1和object2赋值为null，也就是说object1和object2指向的对象已经不可能再被访问，但是由于它们互相引用对方，导致它们的引用计数都不为0，那么垃圾收集器就永远不会回收它们。</span></p>\n<p>&nbsp;</p>\n<p>为了解决这个问题，在Java中采取了 可达性分析法。该方法的基本思想是通过一系列的&ldquo;GC Roots&rdquo;对象作为起点进行搜索，如果在&ldquo;GC Roots&rdquo;和一个对象之间没有可达路径，则称该对象是不可达的，不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程，如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性，则基本上就真的成为可回收对象了。最后面两句将object1和object2赋值为null，也就是说object1和object2指向的对象已经不可能再被访问，但是由于它们互相引用对方，导致它们的引用计数都不为0，那么垃圾收集器就永远不会回收它们。</p>\n<p>Java并不采用引用计数法来判断对象是否已&ldquo;死&rdquo;，而采用&ldquo;可达性分析&rdquo;来判断对象是否存活（同样采用此法的还有C#、Lisp-最早的一门采用动态内存分配的语言）。&nbsp;<br />此算法的核心思想：通过一系列称为&ldquo;GC Roots&rdquo;的对象作为起始点，从这些节点开始向下搜索，搜索走过的路径称为&ldquo;引用链&rdquo;，当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时，证明此对象不可用。以下图为例：</p>\n<p><img src=\"https://img-blog.csdn.net/20180626084654607?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3l1YnVqaWFuX2w=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70\" alt=\"\" width=\"714\" height=\"534\" /></p>\n<p>对象Object5 &mdash;Object7之间虽然彼此还有联系，但是它们到 GC Roots 是不可达的，因此它们会被判定为可回收对象。</p>\n<p>在Java语言中，可作为GC Roots的对象包含以下几种：</p>\n<ol>\n<li>虚拟机栈(栈帧中的本地变量表)中引用的对象。(可以理解为:引用栈帧中的本地变量表的所有对象)</li>\n<li>方法区中静态属性引用的对象(可以理解为:引用方法区该静态属性的所有对象)</li>\n<li>方法区中常量引用的对象(可以理解为:引用方法区中常量的所有对象)</li>\n<li>本地方法栈中(Native方法)引用的对象(可以理解为:<span style=\"color: #ff0000;\">引用Native方法的所有对象</span>)</li>\n</ol>\n<p>可以理解为:</p>\n<p>(1)<span style=\"color: #ff0000;\">首先第一种是虚拟机栈中的引用的对象，我们在程序中正常创建一个对象</span>，对象会在堆上开辟一块空间，同时会将这块空间的地址作为引用保存到虚拟机栈中，如果对象生命周期结束了，那么引用就会从虚拟机栈中出栈，因此如果在虚拟机栈中有引用，就说明这个对象还是有用的，这种情况是最常见的。</p>\n<p>(2)<span style=\"color: #ff0000;\">第二种是我们在类中定义了全局的静态的对象，也就是使用了static关键字</span>，由于虚拟机栈是线程私有的，所以这种对象的引用会保存在共有的方法区中，显然将方法区中的静态引用作为GC Roots是必须的。</p>\n<p>(3)<span style=\"color: #ff0000;\">第三种便是常量引用，就是使用了static final关键字</span>，由于这种引用初始化之后不会修改，所以方法区常量池里的引用的对象也应该作为GC Roots。最后一种是在使用JNI技术时，有时候单纯的Java代码并不能满足我们的需求，我们可能需要在Java中调用C或C++的代码，因此会使用native方法，JVM内存中专门有一块本地方法栈，用来保存这些对象的引用，所以本地方法栈中引用的对象也会被作为GC Roots。</p>\n<p><strong>JVM之判断对象是否存活（引用计数算法、可达性分析算法，最终判定）</strong></p>\n<p><strong>finalize()方法最终判定对象是否存活:</strong></p>\n<p>&nbsp;&nbsp;&nbsp; 即使在可达性分析算法中不可达的对象，也并非是&ldquo;非死不可&rdquo;的，这时候它们暂时处于&ldquo;缓刑&rdquo;阶段，要真正宣告一个对象死亡，至少要经历再次标记过程。<br />&nbsp;&nbsp;&nbsp; 标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。<br />&nbsp;&nbsp;1).第一次标记并进行一次筛选。<br />&nbsp;&nbsp;&nbsp; 筛选的条件是此对象是否有必要执行finalize()方法。<br />&nbsp;&nbsp;&nbsp; 当对象没有覆盖finalize方法，或者finzlize方法已经被虚拟机调用过，虚拟机将这两种情况都视为&ldquo;没有必要执行&rdquo;，对象被回收。<br /><br />&nbsp;&nbsp;2).第二次标记<br />&nbsp;&nbsp;&nbsp; 如果这个对象被判定为有必要执行finalize（）方法，那么这个对象将会被放置在一个名为：F-Queue的队列之中，并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的&ldquo;执行&rdquo;是指虚拟机会触发这个方法，但并不承诺会等待它运行结束。这样做的原因是，如果一个对象finalize（）方法中执行缓慢，或者发生死循环（更极端的情况），将很可能会导致F-Queue队列中的其他对象永久处于等待状态，甚至导致整个内存回收系统崩溃。<br />&nbsp;&nbsp;&nbsp; Finalize（）方法是对象脱逃死亡命运的最后一次机会，稍后GC将对F-Queue中的对象进行第二次小规模标记，如果对象要在finalize（）中成功拯救自己----只要重新与引用链上的任何的一个对象建立关联即可，譬如把自己赋值给某个类变量或对象的成员变量，那在第二次标记时它将移除出&ldquo;即将回收&rdquo;的集合。如果对象这时候还没逃脱，那基本上它就真的被回收了。<br />流程图如下：</p>\n<p><img src=\"https://img-blog.csdn.net/20160514190019348\" alt=\"\" width=\"872\" height=\"341\" /></p>\n<p>在JDK1.2以前，Java中引用的定义很传统: 如果引用类型的数据中存储的数值代表的是另一块内存的起始地址，就称这块内存代表着一个引用。这种定义有些狭隘，一个对象在这种定义下只有被引用或者没有被引用两种状态。&nbsp;<br />我们希望能描述这一类对象: 当内存空间还足够时，则能保存在内存中；如果内存空间在进行垃圾回收后还是非常紧张，则可以抛弃这些对象。很多系统中的缓存对象都符合这样的场景。&nbsp;<br />在JDK1.2之后，Java对引用的概念做了扩充，将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种，这四种引用的强度依次递减。</p>\n<p>⑴强引用（StrongReference）<br />强引用是使用最普遍的引用。如果一个对象具有强引用，那垃圾回收器绝不会回收它。当内存空间不足，Java虚拟机宁愿抛出OutOfMemoryError错误，使程序异常终止，也不会靠随意回收具有强引用的对象来解决内存不足的问题。&nbsp;&nbsp;ps：强引用其实也就是我们平时A a = new A()这个意思。<br /><br />⑵软引用（SoftReference）<br />如果一个对象只具有软引用，则内存空间足够，垃圾回收器就不会回收它；如果内存空间不足了，就会回收这些对象的内存。只要垃圾回收器没有回收它，该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存（下文给出示例）。<br />软引用可以和一个引用队列（ReferenceQueue）联合使用，如果软引用所引用的对象被垃圾回收器回收，Java虚拟机就会把这个软引用加入到与之关联的引用队列中。<br /><br />⑶弱引用（WeakReference）<br />弱引用与软引用的区别在于：只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中，一旦发现了只具有弱引用的对象，不管当前内存空间足够与否，都会回收它的内存。不过，由于垃圾回收器是一个优先级很低的线程，因此不一定会很快发现那些只具有弱引用的对象。<br />弱引用可以和一个引用队列（ReferenceQueue）联合使用，如果弱引用所引用的对象被垃圾回收，Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。<br /><br />⑷虚引用（PhantomReference）<br />&ldquo;虚引用&rdquo;顾名思义，就是形同虚设，与其他几种引用都不同，虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用，那么它就和没有任何引用一样，在任何时候都可能被垃圾回收器回收。<br />虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于：虚引用必须和引用队列 （ReferenceQueue）联合使用。当垃圾回收器准备回收一个对象时，如果发现它还有虚引用，就会在回收对象的内存之前，把这个虚引用加入到与之 关联的引用队列中。</p>\n<p>1 为什么需要使用软引用<br /><br />首先，我们看一个雇员信息查询系统的实例。我们将使用一个Java语言实现的雇员信息查询系统查询存储在磁盘文件或者数据库中的雇员人事档案信息。作为一个用户，我们完全有可能需要回头去查看几分钟甚至几秒钟前查看过的雇员档案信息(同样，我们在浏览WEB页面的时候也经常会使用&ldquo;后退&rdquo;按钮)。</p>\n<p>这时我们通常会有两种程序实现方式:</p>\n<p>一种是:</p>\n<p>把过去查看过的雇员信息保存在内存中，每一个存储了雇员档案信息的Java对象的生命周期贯穿整个应用程序始终;</p>\n<p>另一种是:</p>\n<p>当用户开始查看其他雇员的档案信息的时候，把存储了当前所查看的雇员档案信息的Java对象结束引用，使得垃圾收集线程可以回收其所占用的内存空间，当用户再次需要浏览该雇员的档案信息的时候，重新构建该雇员的信息。</p>\n<p>很显然，第一种实现方法将造成大量的内存浪费.</p>\n<p>而第二种实现的缺陷在于即使垃圾收集线程还没有进行垃圾收集，包含雇员档案信息的对象仍然完好地保存在内存中，应用程序也要重新构建一个对象。</p>\n<p>我们知道，访问磁盘文件、访问网络资源、查询数据库等操作都是影响应用程序执行性能的重要因素，如果能重新获取那些尚未被回收的Java对象的引用，必将减少不必要的访问，大大提高程序的运行速度。</p>\n<p>&nbsp;</p>\n<div id=\"gtx-trans\" style=\"position: absolute; left: -11px; top: 2030px;\">\n<div class=\"gtx-trans-icon\">&nbsp;</div>\n</div>', '在java中是通过引用来和对象进行关联的，也就是说如果要操作对象，必须通过引用来进行。那么很显然一个简单的办法就是通过引用计数来判断一个对象是否可以被回收。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 5, '2019-11-26 14:00:00', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (10, 'Reentrantlock ', '<pre><strong>ReentrantLock 即可重入锁，实现了 Lock 和 Serializable 接口。</strong><br /><strong>在 Java 环境下 ReentrantLock 和 synchronized 都是可重入锁。<br /></strong></pre>\n<div>Lock和synchronized有以下几点不同：</div>\n<ul>\n<li>\n<div>原始结构</div>\n</li>\n<ul>\n<li>\n<div>synchronized 是关键字属于 JVM 层面，反应在字节码上是 monitorenter 和 monitorexit，其底层是通过 monitor 对象来完成，其实 wait/notify 等方法也是依赖 monitor 对象只有在同步快或方法中才能调用 wait/notify 等方法。</div>\n</li>\n<li>\n<div>Lock 是具体类（java.util.concurrent.locks.Lock）是 api 层面的锁。</div>\n</li>\n</ul>\n<li>\n<div>使用方法</div>\n</li>\n<ul>\n<li>\n<div>synchronized 不需要用户手动去释放锁，当 synchronized 代码执行完后系统会自动让线程释放对锁的占用。</div>\n</li>\n<li>\n<div>ReentrantLock 则需要用户手动的释放锁，若没有主动释放锁，可能导致出现死锁的现象，lock() 和 unlock() 方法需要配合 try/finally 语句来完成。</div>\n</li>\n</ul>\n<li>\n<div>等待是否可中断</div>\n</li>\n<ul>\n<li>\n<div>synchronized 不可中断，除非抛出异常或者正常运行完成。</div>\n</li>\n<li>\n<div>ReentrantLock 可中断，设置超时方法 tryLock(long timeout, TimeUnit unit)，lockInterruptibly() 放代码块中，调用 interrupt() 方法可中断。</div>\n</li>\n</ul>\n<li>\n<div>加锁是否公平</div>\n</li>\n<ul>\n<li>\n<div>synchronized 非公平锁</div>\n</li>\n<li>\n<div>ReentrantLock 默认非公平锁，构造方法中可以传入 boolean 值，true 为公平锁，false 为非公平锁。</div>\n</li>\n</ul>\n<li>\n<div>锁可以绑定多个 Condition</div>\n</li>\n<ul>\n<li>\n<div>synchronized 没有 Condition。</div>\n</li>\n<li>\n<div>ReentrantLock 用来实现分组唤醒需要唤醒的线程们，可以精确唤醒，而不是像 synchronized 要么随机唤醒一个线程要么唤醒全部线程。</div>\n</li>\n</ul>\n</ul>\n<p>一个Reentrantlock版本的生产者消费者</p>\n<pre class=\"language-java\"><code>import java.util.concurrent.locks.Condition;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\n\npublic class AirCondition {\n    private int num = 0;\n    private Lock lock = new ReentrantLock();\n    private Condition condition = lock.newCondition();\n\n    private void production() {\n        lock.lock();\n        try {\n            //1.判断\n            while (num &gt;= 1) {\n                condition.await();//代替this.wait()\n            }\n            //2.执行\n            this.num++;\n            System.out.println(Thread.currentThread().getName() + \"生产1个此时资源量：\" + num);\n            //3.通知\n            condition.signalAll();//代替notifyAll()\n\n        } catch (Exception e) {\n            e.printStackTrace();\n        } finally {\n            lock.unlock();\n        }\n\n\n    }\n\n    private void consume() {\n        lock.lock();\n        try {\n            while (num &lt;= 0) {\n                condition.await();\n            }\n            this.num--;\n            System.out.println(Thread.currentThread().getName() + \"消费1个此时资源量：\" + num);\n            condition.signalAll();\n        } catch (Exception e) {\n            e.printStackTrace();\n        } finally {\n            lock.unlock();\n        }\n\n\n    }\n\n    public static void main(String[] args) {\n        AirCondition airCondition = new AirCondition();\n        new Thread(() -&gt; {\n            for (int i = 0; i &lt; 10; i++) {\n              airCondition.consume();\n            }\n\n        }, \"消费者1\").start();\n\n        new Thread(() -&gt; {\n            for (int i = 0; i &lt; 10; i++) {\n                airCondition.consume();\n            }\n\n        }, \"消费者2\").start();\n\n        new Thread(() -&gt; {\n            for (int i = 0; i &lt; 10; i++) {\n                airCondition.production();\n            }\n\n        }, \"生产者1\").start();\n\n        new Thread(() -&gt; {\n            for (int i = 0; i &lt; 10; i++) {\n                airCondition.production();\n            }\n\n        }, \"生产者2\").start();\n    }\n\n}\n</code></pre>\n<p>多线程之间按顺序调用实现 p1-&gt;p2-&gt;p3</p>\n<p>三个线程启动，p1 输出111 p2输出222 p3输出333 来十轮，如下</p>\n<pre class=\"language-java\"><code>import java.util.concurrent.locks.Condition;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\n\npublic class AirCondition {\n    private int flag = 1;\n    private Lock lock = new ReentrantLock();\n    private Condition c1 = lock.newCondition();//print1\n    private Condition c2 = lock.newCondition();//print2\n    private Condition c3 = lock.newCondition();//print3\n\n    private void print1() {\n        lock.lock();\n        try {\n            //1.判断\n            while (flag != 1) {\n                c1.await();//代替this.wait()\n            }\n            //2.执行\n            System.out.println(Thread.currentThread().getName()+\" 111\");\n            //3.通知下一个\n            flag=2;\n            c2.signal();\n        } catch (Exception e) {\n            e.printStackTrace();\n        } finally {\n            lock.unlock();\n        }\n    }\n\n    private void print2() {\n        lock.lock();\n        try {\n            //1.判断\n            while (flag != 2) {\n                c2.await();//代替this.wait()\n            }\n            //2.执行\n            System.out.println(Thread.currentThread().getName()+\" 222\");\n            //3.通知下一个\n            flag=3;\n            c3.signal();\n        } catch (Exception e) {\n            e.printStackTrace();\n        } finally {\n            lock.unlock();\n        }\n    }\n\n    private void print3() {\n        lock.lock();\n        try {\n            //1.判断\n            while (flag != 3) {\n                c3.await();//代替this.wait()\n            }\n            //2.执行\n            System.out.println(Thread.currentThread().getName()+\" 333\");\n            //3.通知下一个\n            flag=1;\n            c1.signal();\n        } catch (Exception e) {\n            e.printStackTrace();\n        } finally {\n            lock.unlock();\n        }\n    }\n\n\n\n    public static void main(String[] args) {\n        AirCondition airCondition = new AirCondition();\n        new Thread(() -&gt; {\n            for (int i = 0; i &lt; 10; i++) {\n              airCondition.print1();\n            }\n\n        }, \"p1\").start();\n\n        new Thread(() -&gt; {\n            for (int i = 0; i &lt; 10; i++) {\n                airCondition.print2();\n            }\n\n        }, \"p2\").start();\n\n        new Thread(() -&gt; {\n            for (int i = 0; i &lt; 10; i++) {\n                airCondition.print3();\n            }\n\n        }, \"p3\").start();\n\n\n    }   </code></pre>\n<pre class=\"language-java\"><code>p1 111\np2 222\np3 333\np1 111\np2 222\np3 333\np1 111\np2 222\np3 333\np1 111\np2 222\np3 333\np1 111\np2 222\np3 333\np1 111\np2 222\np3 333\np1 111\np2 222\np3 333\np1 111\np2 222\np3 333\np1 111\np2 222\np3 333\np1 111\np2 222\np3 333</code></pre>', 'ReentrantLock 即可重入锁，实现了 Lock 和 Serializable 接口。在 Java 环境下 ReentrantLock 和 synchronized 都是可重入锁。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 1, 1, 1, 1, 1, '2019-11-26 14:00:00', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (11, '对 ThreadPoolExector 的理解', '<h3>为什使用线程池，线程池的优势？</h3>\n<div>线程池用于多线程处理中，它可以根据系统的情况，可以有效控制线程执行的数量，优化运行效果。线程池做的工作主要是控制运行的线程的数量，处理过程中将任务放入队列，然后在线程创建后启动这些任务，如果线程数量超过了最大数量，那么超出数量的线程排队等候，等其它线程执行完毕，再从队列中取出任务来执行。</div>\n<div>主要特点为：</div>\n<ul>\n<li>\n<div>线程复用</div>\n</li>\n<li>\n<div>控制最大并发数量</div>\n</li>\n<li>\n<div>管理线程</div>\n</li>\n</ul>\n<div>主要优点</div>\n<ul>\n<li>\n<div>降低资源消耗，通过重复利用已创建的线程来降低线程创建和销毁造成的消耗。</div>\n</li>\n<li>\n<div>提高相应速度，当任务到达时，任务可以不需要的等到线程创建就能立即执行。</div>\n</li>\n<li>\n<div>提高线程的可管理性，线程是稀缺资源，如果无限制的创建，不仅仅会消耗系统资源，还会降低体统的稳定性，使用线程可以进行统一分配，调优和监控。</div>\n</li>\n</ul>\n<h3>创建线程的几种方式</h3>\n<ul>\n<li>\n<div>继承 Thread</div>\n</li>\n<li>\n<div>实现 Runnable 接口</div>\n</li>\n<li>\n<div>实现 Callable</div>\n</li>\n<li>使用线程池</li>\n</ul>\n<h4>编码实现</h4>\n<ul>\n<li>\n<div>Executors.newSingleThreadExecutor()：只有一个线程的线程池，因此所有提交的任务是顺序执行</div>\n</li>\n<li>\n<div>Executors.newCachedThreadPool()：线程池里有很多线程需要同时执行，老的可用线程将被新的任务触发重新执行，如果线程超过60秒内没执行，那么将被终止并从池中删除</div>\n</li>\n<li>\n<div>Executors.newFixedThreadPool()：拥有固定线程数的线程池，如果没有任务执行，那么线程会一直等待</div>\n</li>\n<li>\n<div>Executors.newScheduledThreadPool()：用来调度即将执行的任务的线程池</div>\n</li>\n<li>\n<div>Executors.newWorkStealingPool()： newWorkStealingPool适合使用在很耗时的操作，但是newWorkStealingPool不是ThreadPoolExecutor的扩展，它是新的线程池类ForkJoinPool的扩展，但是都是在统一的一个Executors类中实现，由于能够合理的使用CPU进行对任务操作（并行操作），所以适合使用在很耗时的任务中</div>\n</li>\n</ul>\n<h4>ThreadPoolExecutor</h4>\n<div>ThreadPoolExecutor作为java.util.concurrent包对外提供基础实现，以内部线程池的形式对外提供管理任务执行，线程调度，线程池管理等等服务。</div>\n<h3>线程池的几个重要参数介绍？</h3>\n<ol>\n<li>corePoolSize：线程池的核心线程数</li>\n<li>maximumPoolSize：线程池的最大线程数，此值必须大于等于1</li>\n<li>keepAliveTime：多余的空闲线程的存活时间，线程池数量超过corePoolSize，空闲时间达到keepAliveTime值时，多余空闲线程会被销毁直到剩下corePoolSize个线程为止</li>\n<li>TimeUnit：keepAliveTime的单位</li>\n<li>workQueue：任务队列，被提交但是尚未被执行的任务</li>\n<li>threadFactory：设置创建线程的工厂</li>\n<li class=\"p1\">RejectedExecutionHandler：拒绝策略，当提交任务数超过maxmumPoolSize+workQueue之和时，任务会交给RejectedExecutionHandler 来处理</li>\n</ol>\n<div><br />重点讲解： 其中比较容易让人误解的是：corePoolSize，maximumPoolSize，workQueue之间关系。说说线程池的底层工作原理？</div>\n<ol>\n<li>\n<div>当线程池小于corePoolSize时，新提交任务将创建一个新线程执行任务，即使此时线程池中存在空闲线程。</div>\n</li>\n<li>\n<div>当线程池达到corePoolSize时，新提交任务将被放入 workQueue 中，等待线程池中任务调度执行。</div>\n</li>\n<li>\n<div>当workQueue已满，且 maximumPoolSize 大于 corePoolSize 时，新提交任务会创建新线程执行任务。</div>\n</li>\n<li>\n<div>当提交任务数超过 maximumPoolSize 时，新提交任务由 RejectedExecutionHandler 处理。</div>\n</li>\n<li>\n<div>当线程池中超过corePoolSize 线程，空闲时间达到 keepAliveTime 时，关闭空闲线程 。</div>\n</li>\n<li>\n<div>当设置allowCoreThreadTimeOut(true) 时，线程池中 corePoolSize 线程空闲时间达到 keepAliveTime 也将关闭。</div>\n</li>\n</ol>\n<p><img src=\"http://blog.cuzz.site/2019/04/16/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/92ad4409-2ab4-388b-9fb1-9fc4e0d832cd.jpg\" alt=\"\" /></p>\n<h3 id=\"线程池的拒绝策略你谈谈？\">线程池的拒绝策略你谈谈？</h3>\n<ul>\n<li>是什么\n<ul>\n<li>等待队列已经满了，再也塞不下新的任务，同时线程池中的线程数达到了最大线程数，无法继续为新任务服务。</li>\n</ul>\n</li>\n<li>拒绝策略\n<ul>\n<li>AbortPolicy（默认）：直接抛出现RejectedExecutionException异常。</li>\n<li>CallerRunsPolicy：该策略既不会抛弃任务，也不会抛出异常，而是将某些任务回退到调用者，从而降低新任务的流量</li>\n<li>DiscardOldestPolicy：丢弃队列里等待最久的任务，然后把当前任务加入队列中尝试再次提交当前任务。</li>\n<li>DiscardPolicy：直接丢弃掉，不予任何处理也不抛出异常。</li>\n</ul>\n</li>\n</ul>\n<h3 id=\"你在工作中单一的、固定数的和可变的三种创建线程池的方法，你用哪个多，超级大坑？\">你在工作中单一的、固定数的和可变的三种创建线程池的方法，你用哪个多，超级大坑？</h3>\n<p>如果读者对Java中的阻塞队列有所了解的话，看到这里或许就能够明白原因了。</p>\n<p>Java中的BlockingQueue主要有两种实现，分别是ArrayBlockingQueue 和 LinkedBlockingQueue。</p>\n<p>ArrayBlockingQueue是一个用数组实现的有界阻塞队列，必须设置容量。</p>\n<p>LinkedBlockingQueue是一个用链表实现的有界阻塞队列，容量可以选择进行设置，不设置的话，将是一个无边界的阻塞队列，最大长度为Integer.MAX_VALUE。</p>\n<p>这里的问题就出在：不设置的话，将是一个无边界的阻塞队列，最大长度为Integer.MAX_VALUE。也就是说，如果我们不设置LinkedBlockingQueue的容量的话，其默认容量将会是Integer.MAX_VALUE。</p>\n<p>而newFixedThreadPool中创建LinkedBlockingQueue时，并未指定容量。此时，LinkedBlockingQueue就是一个无边界队列，对于一个无边界队列来说，是可以不断的向队列中加入任务的，这种情况下就有可能因为任务过多而导致内存溢出问题。</p>\n<p>上面提到的问题主要体现在newFixedThreadPool和newSingleThreadExecutor两个工厂方法上，并不是说newCachedThreadPool和newScheduledThreadPool这两个方法就安全了，这两种方式创建的最大线程数可能是Integer.MAX_VALUE，而创建这么多线程，必然就有可能导致OOM。</p>\n<h3 id=\"你在工作中是如何使用线程池的，是否自定义过线程池使用？\">你在工作中是如何使用线程池的，是否自定义过线程池使用？</h3>\n<p>自定义线程池</p>\n<pre class=\"language-java\"><code>public class ThreadPoolExecutorDemo {\n\n    public static void main(String[] args) {\n        Executor executor = new ThreadPoolExecutor(2, 3, 1L, TimeUnit.SECONDS,\n                new LinkedBlockingQueue&lt;&gt;(5), \n                Executors.defaultThreadFactory(), \n                new ThreadPoolExecutor.DiscardPolicy());\n    }\n}</code></pre>\n<h3 id=\"合理配置线程池你是如果考虑的？\">合理配置线程池你是如果考虑的？</h3>\n<ul>\n<li>CPU 密集型\n<ul>\n<li>CPU 密集的意思是该任务需要大量的运算，而没有阻塞，CPU 一直全速运行。</li>\n<li>CPU 密集型任务尽可能的少的线程数量，一般为 CPU 核数 + 1 个线程的线程池。</li>\n</ul>\n</li>\n<li>IO 密集型\n<ul>\n<li>由于 IO 密集型任务线程并不是一直在执行任务，可以多分配一点线程数，如 CPU * 2 。</li>\n<li>也可以使用公式：CPU 核数 / (1 - 阻塞系数)；其中阻塞系数在 0.8 ～ 0.9 之间。</li>\n</ul>\n</li>\n</ul>', '线程池用于多线程处理中，它可以根据系统的情况，可以有效控制线程执行的数量，优化运行效果。线程池做的工作主要是控制运行的线程的数量，处理过程中将任务放入队列，然后在线程创建后启动这些任务，如果线程数量超过了最大数量，那么超出数量的线程排队等候，等其它线程执行完毕，再从队列中取出任务来执行。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 1, 1, 1, 1, 1, '2019-11-26 14:00:00', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (12, 'CountDownLatch', '<div>\n<p>countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。</p>\n<p>是通过一个计数器来实现的，计数器的初始值是线程的数量。每当一个线程执行完毕后，计数器的值就-1，当计数器的值为0时，表示所有线程都执行完毕，然后在闭锁上等待的线程就可以恢复工作了。</p>\n<p>countDownLatch类中只提供了一个构造器：</p>\n<pre class=\"line-numbers  language-cpp\"><code>//参数count为计数值\npublic CountDownLatch(int count) {  };</code></pre>\n<pre class=\"line-numbers  language-java\"><code>//调用await()方法的线程会被挂起，它会等待直到count值为0才继续执行\npublic void await() throws InterruptedException { };   \n//和await()类似，只不过等待一定的时间后count值还没变为0的话就会继续执行\npublic boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  \n//将count值减1\npublic void countDown() { };  </code></pre>\n</div>\n<p>演示：教师有6个同学和班长，班长需要等6个同学都走后才能锁门。</p>\n<pre class=\"language-java\"><code>import java.util.concurrent.CountDownLatch;\n\npublic class CountdownLatchDemo {\n    public static void main(String[] args) throws InterruptedException {\n        CountDownLatch countDownLatch=new CountDownLatch(6);//6个线程\n        for (int i=0;i&lt;6;i++){\n            new Thread(()-&gt;{\n                System.out.println(Thread.currentThread().getName()+ \"学生离开教室\");\n                countDownLatch.countDown();\n            },String.valueOf(i)).start();\n        }\n        countDownLatch.await();//6线程个执行完才执行下面\n        System.out.println(\"班长锁门！\");\n\n    }\n}\n</code></pre>\n<pre class=\"language-java\"><code>0学生离开教室\n1学生离开教室\n3学生离开教室\n4学生离开教室\n2学生离开教室\n5学生离开教室\n班长锁门！</code></pre>\n<hr />\n<p><strong>场景说明：</strong></p>\n<ul>\n<li>模拟多线程分组计算</li>\n<li>有一个大小为50000的随机数组，用5个线程分别计算10000个元素的和</li>\n<li>然后在将计算结果进行合并，得出最后的结果。</li>\n</ul>\n<pre class=\"language-java\"><code>import java.util.Arrays;\nimport java.util.Random;\nimport java.util.concurrent.CountDownLatch;\n\npublic class CountdownLatchDemo {\n    public static void main(String[] args) throws InterruptedException {\n        //数组大小\n        int size = 50000;\n        //定义数组\n        int[] numbers = new int[size];\n        //随机初始化数组\n        Random random = new Random();\n        for (int i = 0; i &lt; size; i++) {\n            numbers[i] = random.nextInt(100);\n        }\n\n        //单线程计算结果\n        System.out.println();\n        Long sum = 0L;\n        for (int i = 0; i &lt; size; i++) {\n            sum += numbers[i];\n        }\n        System.out.println(\"单线程计算结果：\" + sum);\n//----------------------------------------------------------------------------------------------------------------------\n        //定义五个Future去保存子数组计算结果\n        final int[] results = new int[5];\n        //子数组长度\n        int length = 10000;\n        CountDownLatch countDownLatch=new CountDownLatch(5);//5个线程\n        for (int i=0;i&lt;5;i++){\n            int finalI = i;\n            int[] subNumbers = Arrays.copyOfRange(numbers, (i * length), ((i + 1) * length)); //每个线程计算一份数组\n            new Thread(()-&gt;{\n                for (int j = 0; j &lt;subNumbers.length ; j++) {\n                    results[finalI]+=subNumbers[j];\n                }\n                countDownLatch.countDown();\n            },String.valueOf(i)).start();\n        }\n        countDownLatch.await();//5个线程个执行完才执行下面\n        int sums = 0;\n        for (int i = 0; i &lt; 5; i++) {\n            sums += results[i];\n        }\n        System.out.println(\"多线程计算结果：\" + sums);\n\n    }\n}\n</code></pre>\n<pre class=\"language-java\"><code>单线程计算结果：2483567\n多线程计算结果：2483567</code></pre>', 'countDownLatch是在java1.5被引入，跟它一起被引入的工具类还有CyclicBarrier、Semaphore、concurrentHashMap和BlockingQueue。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 1, 1, 1, 1, 1, '2019-11-26 14:00:00', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (13, 'CyclicBarrier', '<p>CyclicBarrier，是JDK1.5的java.util.concurrent并发包中提供的一个并发工具类。</p>\n<p>所谓Cyclic即 循环 的意思，所谓Barrier即 屏障 的意思。</p>\n<p>所以综合起来，CyclicBarrier指的就是 循环屏障，虽然这个叫法很奇怪，但是确能很好地表示它的作用。<br /><br /></p>\n<hr />\n<h3 id=\"2cyclicbarrier方法说明\">CyclicBarrier方法说明</h3>\n<h4><span style=\"background-color: #00ccff;\">CyclicBarrier(parties,Runnable barrierAction)</span></h4>\n<h5>初始化相互等待的线程数量以及屏障线程的构造方法。屏障线程的运行时机：等待的线程数量=parties之后，CyclicBarrier打开屏障之前。</h5>\n<h4><span style=\"background-color: #00ccff;\">await()</span></h4>\n<h5>在CyclicBarrier上进行阻塞等待，直到发生以下情形之一：</h5>\n<ul>\n<li>\n<h5>在CyclicBarrier上等待的线程数量达到parties，则所有线程被释放，继续执行。</h5>\n</li>\n<li>\n<h5>当前线程被中断，则抛出InterruptedException异常，并停止等待，继续执行。</h5>\n</li>\n<li>\n<h5>当前线程被中断，则抛出InterruptedException异常，并停止等待，继续执行。</h5>\n</li>\n<li>\n<h5>其他等待的线程被中断，则当前线程抛出BrokenBarrierException异常，并停止等待，继续执行。</h5>\n</li>\n<li>\n<h5>其他线程调用CyclicBarrier.reset()方法，则当前线程抛出BrokenBarrierException异常，并停止等待，继续执行。</h5>\n</li>\n</ul>\n<p><span style=\"background-color: #00ccff;\"><strong>await(timeout,TimeUnit)</strong></span></p>\n<h5>在CyclicBarrier上进行限时的阻塞等待</h5>\n<hr />\n<h5>应用场景：收集齐7个龙珠可以召唤神龙，召唤后，还可以收集召唤。。。</h5>\n<pre class=\"language-java\"><code>import java.util.concurrent.BrokenBarrierException;\nimport java.util.concurrent.CyclicBarrier;\n\npublic class CyclicBarrierDemo {\n    public static void main(String[] args) {\n        CyclicBarrier cyclicBarrier=new CyclicBarrier(7,()-&gt;{System.out.println(\"召唤神龙！\");});\n        for (int i = 0; i &lt;14 ; i++) {\n            final int tem=i;\n            new Thread(()-&gt;{\n                System.out.println(Thread.currentThread().getName()+\"收集到第\"+tem+\"个龙珠\");\n                try {\n                    cyclicBarrier.await();\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                } catch (BrokenBarrierException e) {\n                    e.printStackTrace();\n                }\n            },String.valueOf(i)).start();\n        }\n    }\n}\n</code></pre>\n<pre class=\"language-java\"><code>0收集到第0个龙珠\n1收集到第1个龙珠\n2收集到第2个龙珠\n3收集到第3个龙珠\n4收集到第4个龙珠\n5收集到第5个龙珠\n6收集到第6个龙珠\n召唤神龙！\n7收集到第7个龙珠\n8收集到第8个龙珠\n11收集到第11个龙珠\n12收集到第12个龙珠\n9收集到第9个龙珠\n10收集到第10个龙珠\n13收集到第13个龙珠\n召唤神龙！</code></pre>\n<p>&nbsp;</p>', 'CyclicBarrier，是JDK1.5的java.util.concurrent并发包中提供的一个并发工具类。所谓Cyclic即 循环 的意思，所谓Barrier即 屏障 的意思。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 1, '2019-11-26 14:00:00', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (43, 'Semaphore', '<p>Semaphore和ReentrantLock类似，获取许可有公平策略和非公平许可策略，默认情况下使用非公平策略。</p>\n<p>　　当初始值为1时，可以用作互斥锁，并具备不可重入的加锁语义。</p>\n<p>　　Semaphore将AQS的同步状态用于保存当前可用许可的数量。</p>\n<p><strong>实现资源池：</strong></p>\n<p>　　一个固定长度的资源池，当池为空时，请求资源会失败。</p>\n<p>　　使用Semaphore可以实现当池为空时，请求会阻塞，非空时解除阻塞。</p>\n<p>　　也可以使用Semaphore将任何一种容器变成有界阻塞容器。</p>\n<p>应用场景：</p>\n<p>以一个停车场是运作为例。假设停车场只有三个车位，一开始三个车位都是空的。这时如果同时来了六辆车，看门人允许其中三辆不受阻碍的进入，然后放下车拦，剩下的车则必须在入口等待，此后来的车也都不得不在入口处等待。这时，有一辆车离开停车场，看门人得知后，打开车拦，放入一辆，如果又离开两辆，则又可以放入两辆，如此往复。</p>\n<p>这个停车系统中，每辆车就好比一个线程，看门人就好比一个信号量，看门人限制了可以活动的线程。假如里面依然是三个车位，但是看门人改变了规则，要求每次只能停两辆车，那么一开始进入两辆车，后面得等到有车离开才能有车进入，但是得保证最多停两辆车。对于Semaphore类而言，就如同一个看门人，限制了可活动的线程数。<br /><br /></p>\n<pre class=\"language-java\"><code>import java.util.concurrent.Semaphore;\n\npublic class SemaphoreDemo {\n    public static void main(String[] args) {\n        Semaphore semaphore=new Semaphore(3);//最多3个车位\n        for (int i = 0; i &lt; 6; i++) {//6辆车\n            new Thread(()-&gt;{\n                try {\n                    semaphore.acquire();//占用车位\n                    System.out.println(Thread.currentThread().getName()+\" 抢到车位。\");\n                    Thread.sleep(2000);\n                    System.out.println(Thread.currentThread().getName()+\" 离开车位。\");\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }finally {\n                    semaphore.release();//释放信车位\n                }\n            },String.valueOf(i)).start();\n        }\n    }\n}\n</code></pre>\n<pre class=\"language-java\"><code>1 抢到车位。\n2 抢到车位。\n3 抢到车位。\n\n2 离开车位。\n3 离开车位。\n1 离开车位。\n5 抢到车位。\n0 抢到车位。\n4 抢到车位。\n\n0 离开车位。\n5 离开车位。\n4 离开车位。</code></pre>', 'Semaphore也叫信号量，在JDK1.5被引入，用来控制同时访问某个特定资源的操作数量，或者同时执行某个指定操作的数量。还可以用来实现某种资源池，或者对容器施加边界。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 1, '2019-11-30 19:48:23', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (44, 'BlockingQueue', '<p>BlockingQueue即阻塞队列，它是基于ReentrantLock，依据它的基本原理，我们可以实现Web中的长连接聊天功能，当然其最常用的还是用于实现生产者与消费者模式，大致如下图所示：</p>\n<p><img src=\"https://upload-images.jianshu.io/upload_images/4180398-89f0d2693361656e.png?imageMogr2/auto-orient/strip|imageView2/2/w/873/format/webp\" alt=\"\" /></p>\n<p>概念：</p>\n<ul>\n<li>当队列是空的，试图获取元素将阻塞</li>\n<li>当队列是满的，试图添加元素将阻塞</li>\n</ul>\n<p>为什么需要阻塞队列：</p>\n<p>我们不需要关心什么时候阻塞线程，一旦条件满足，被挂起的线程会自动唤醒。</p>\n<p>主要实现类：</p>\n<ul>\n<li>ArrayBlockingQueue：由数组组成的有界队列</li>\n<li>LinkedBlockingQueue：由链表组成的有界队列（默认大小是Integer.MAX&mdash;VALUE）</li>\n<li>SynchronousQueue：不存储元素的阻塞队列，即<span style=\"color: #ff0000;\">单个元素</span>的队列</li>\n<li>LinkedBlockingDeque：由链表组成的<span style=\"color: #ff0000;\">双向</span>阻塞队列。</li>\n</ul>\n<hr />\n<p>核心方法：</p>\n<table style=\"border-collapse: collapse; width: 53.1522%;\" border=\"1\">\n<tbody>\n<tr>\n<td style=\"width: 12.5%; text-align: center;\">方法类型</td>\n<td style=\"width: 12.5%; text-align: center;\">抛出异常</td>\n<td style=\"width: 12.5%; text-align: center;\">特殊值</td>\n<td style=\"width: 12.5%; text-align: center;\">阻塞</td>\n<td style=\"width: 12.5%; text-align: center;\">超时</td>\n</tr>\n<tr>\n<td style=\"width: 12.5%; text-align: center;\">插入</td>\n<td style=\"width: 12.5%; text-align: center;\">add（e）</td>\n<td style=\"width: 12.5%; text-align: center;\">offer（e）</td>\n<td style=\"width: 12.5%; text-align: center;\">put（e）</td>\n<td style=\"width: 12.5%; text-align: center;\">offer（e，time，unit）</td>\n</tr>\n<tr>\n<td style=\"width: 12.5%; text-align: center;\">移除</td>\n<td style=\"width: 12.5%; text-align: center;\">remove（）</td>\n<td style=\"width: 12.5%; text-align: center;\">poll（）</td>\n<td style=\"width: 12.5%; text-align: center;\">take（）</td>\n<td style=\"width: 12.5%; text-align: center;\">poll（e，unit）</td>\n</tr>\n<tr>\n<td style=\"width: 12.5%; text-align: center;\">检查</td>\n<td style=\"width: 12.5%; text-align: center;\">element（）</td>\n<td style=\"width: 12.5%; text-align: center;\">peek（）</td>\n<td style=\"width: 12.5%; text-align: center;\">不可用</td>\n<td style=\"width: 12.5%; text-align: center;\">不可用</td>\n</tr>\n</tbody>\n</table>\n<p>&nbsp;</p>\n<table style=\"border-collapse: collapse; width: 100%;\" border=\"1\">\n<tbody>\n<tr>\n<td style=\"width: 50%; text-align: center;\">抛出异常</td>\n<td style=\"width: 50%; text-align: center;\">\n<p>阻塞队列满，执行add抛出异常：IllegalStateException: Queue full</p>\n<p>阻塞队列空，remove抛出异常：NoSuchElementException</p>\n</td>\n</tr>\n<tr>\n<td style=\"width: 50%; text-align: center;\">特殊值</td>\n<td style=\"width: 50%; text-align: center;\">\n<p>插入 成功true，失败false</p>\n<p>移除 成功返回出队列的元素，失败返回null</p>\n</td>\n</tr>\n<tr>\n<td style=\"width: 50%; text-align: center;\">一直阻塞</td>\n<td style=\"width: 50%; text-align: center;\">\n<p>阻塞度列满时，生产者继续put，队列一直阻塞线程知道put or 响应中断退出</p>\n<p>阻塞队列空时，消费者take元素，队列会一直阻塞消费线程直到队列有数据</p>\n</td>\n</tr>\n<tr>\n<td style=\"width: 50%; text-align: center;\">超时退出</td>\n<td style=\"width: 50%; text-align: center;\">阻塞队列满时，队列阻塞生产者一段时间，超市后生产者线程退出</td>\n</tr>\n</tbody>\n</table>\n<p>&nbsp;</p>', 'BlockingQueue即阻塞队列，它是基于ReentrantLock，依据它的基本原理，我们可以实现Web中的长连接聊天功能，当然其最常用的还是用于实现生产者与消费者模式', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 1, '2019-12-02 21:43:22', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (45, '61. 旋转链表', '<p>给定一个链表，旋转链表，将链表每个节点向右移动&nbsp;k&nbsp;个位置，其中&nbsp;k&nbsp;是非负数。</p>\n<p>示例&nbsp;1:</p>\n<p>输入: 1-&gt;2-&gt;3-&gt;4-&gt;5-&gt;NULL, k = 2<br />输出: 4-&gt;5-&gt;1-&gt;2-&gt;3-&gt;NULL<br />解释:<br />向右旋转 1 步: 5-&gt;1-&gt;2-&gt;3-&gt;4-&gt;NULL<br />向右旋转 2 步: 4-&gt;5-&gt;1-&gt;2-&gt;3-&gt;NULL<br />示例&nbsp;2:</p>\n<p>输入: 0-&gt;1-&gt;2-&gt;NULL, k = 4<br />输出: 2-&gt;0-&gt;1-&gt;NULL<br />解释:<br />向右旋转 1 步: 2-&gt;0-&gt;1-&gt;NULL<br />向右旋转 2 步: 1-&gt;2-&gt;0-&gt;NULL<br />向右旋转 3 步:&nbsp;0-&gt;1-&gt;2-&gt;NULL<br />向右旋转 4 步:&nbsp;2-&gt;0-&gt;1-&gt;NULL</p>\n<hr />\n<p>思路： 环链表法<br />计算链表的长度N。</p>\n<p>k对N取模。 r=k%N。</p>\n<p>当 r == 0 时，不需要旋转。</p>\n<p>当 r &gt; 0 时，右移r个节点即可。</p>\n<p>将链表的尾部指向头部，构成循环链表。并在从开始的N-r个节点后面将链表断开。第N-r+1个节点就是新的头节点。</p>\n<p>例如：1-&gt;2-&gt;3-&gt;4-&gt;5-&gt;NULL, k = 2</p>\n<p>链表长度N=5. 尾部连到头部构成环。 1-&gt;2-&gt;3-&gt;4-&gt;5-&gt;</p>\n<p>r=k%N=2%5=2. 右移2个节点。在第N-r=5-2=3个节点后面断开，第N-r+1=5-2+1=4节点就是新的头节点。</p>\n<pre class=\"language-java\"><code> public ListNode rotateRight(ListNode head, int k) {\n        if(head==null)\n            return null;\n        int len=1;\n        ListNode tem=head;\n        while(tem.next!=null)//第一次遍历找到长度和尾结点\n        {\n            len++;\n            tem=tem.next;\n        }\n        tem.next=head;//首尾相连组成循环链表\n        int btnum=k%len;//真正需要右移的次数\n        ListNode interruptNode=head;    \n        for (int i = 0; i &lt; len - btnum-1; i++) {//第二次遍历找到断开节点\n            interruptNode=interruptNode.next;\n        }\n        ListNode res=interruptNode.next;\n        interruptNode.next=null;\n        return res;\n    }</code></pre>\n<hr />\n<h3>提交记录</h3>\n<div id=\"details-summary\">\n<table>\n<tbody>\n<tr>\n<td>\n<div id=\"result_progress_row\" class=\"row\"><strong><span id=\"result_progress\" class=\"ng-binding\">231 / 231</span></strong>&nbsp;个通过测试用例</div>\n</td>\n<td id=\"status\">状态：\n<h4 id=\"result_state\" class=\"inline-wrap ng-binding text-success\">通过</h4>\n</td>\n</tr>\n<tr>\n<td>\n<div id=\"ac_output\" class=\"row\">执行用时：<strong><span id=\"result_runtime\" class=\"ng-binding\">1 ms</span></strong></div>\n</td>\n<td>\n<div id=\"submitted-time\">提交时间：<strong><span id=\"result_date\">1&nbsp;小时，33&nbsp;分钟之前</span></strong></div>\n</td>\n</tr>\n</tbody>\n</table>\n</div>', '61. 旋转链表', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 2, '2019-12-03 13:35:34', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (46, '82. 删除排序链表中的重复元素 II', '<p>给定一个排序链表，删除所有含有重复数字的节点，只保留原始链表中&nbsp;没有重复出现&nbsp;的数字。</p>\n<p>示例&nbsp;1:</p>\n<p>输入: 1-&gt;2-&gt;3-&gt;3-&gt;4-&gt;4-&gt;5<br />输出: 1-&gt;2-&gt;5</p>\n<p><br />示例&nbsp;2:</p>\n<p>输入: 1-&gt;1-&gt;1-&gt;2-&gt;3<br />输出: 2-&gt;3</p>\n<hr />\n<p>思路一：使用map记录节点值将所有出现次数大于1的值删除。需要遍历两次并且使用散列表辅助空间。</p>\n<pre class=\"language-java\"><code>ListNode deleteDuplicates(ListNode head) {\n        Map&lt;Integer,Boolean&gt; map=new HashMap&lt;&gt;();\n        ListNode tem=head;\n        while(tem!=null){\n            if(map.containsKey(tem.val)){\n                map.put(tem.val,true);\n            }else{\n                map.put(tem.val,false);\n            }\n            tem=tem.next;\n        }\n        ListNode node=new ListNode(0);\n        node.next=head;\n        ListNode res=node;\n        while(node.next!=null){\n            if(map.get(node.next.val))\n            {\n                node.next=node.next.next;\n            }else {\n                node=node.next;\n            }\n        }\n        return res.next;\n    }</code></pre>\n<div class=\"css-fap0zx-ResultInfo e18r7j6f3\">执行用时 :5 ms, 在所有&nbsp;java&nbsp;提交中击败了5.79%的用户</div>\n<div class=\"css-fap0zx-ResultInfo e18r7j6f3\">内存消耗 :36.6 MB, 在所有&nbsp;java&nbsp;提交中击败了62.44%的用户</div>\n<div class=\"css-fap0zx-ResultInfo e18r7j6f3\">嗯。。。慢的一批。。尝试其他思路。。</div>\n<div class=\"css-fap0zx-ResultInfo e18r7j6f3\"><hr /></div>\n<div class=\"css-fap0zx-ResultInfo e18r7j6f3\">思路二：对于每一个节点比较 它后面两个元素是否相等，如果相等继续判断第三，四，五...个是否相等，然后删除这些节点。如果不相等，往后遍历。</div>\n<div class=\"css-fap0zx-ResultInfo e18r7j6f3\">\n<pre class=\"language-java\"><code>  public ListNode deleteDuplicates(ListNode head) {\n        ListNode lshead = new ListNode(0);//接一个头结点\n        lshead.next = head;\n        ListNode tem = lshead;//正在遍历的节点\n        ListNode prenode = tem.next;//遍历节点的后继\n        ListNode nextnode = null;//遍历节点的后继的后继\n        if (prenode != null) {\n            nextnode = prenode.next;\n        }\n       while (prenode != null &amp;&amp; nextnode != null) {\n            while (nextnode != null&amp;&amp;prenode.val == nextnode.val) {//相等则nextnode继续往后遍历\n                nextnode = nextnode.next;\n                tem.next = nextnode;\n            }\n            if(nextnode!=null){//删除重复节点\n                prenode = tem.next;\n                nextnode = prenode.next;\n            }\n            while (nextnode != null &amp;&amp; prenode.val != nextnode.val) {//不相等往后遍历\n                tem = tem.next;\n                prenode = tem.next;\n                nextnode = tem.next.next;\n\n            }\n        }\n        return lshead.next;\n    }</code></pre>\n<div class=\"css-fap0zx-ResultInfo e18r7j6f3\">执行用时 :1 ms, 在所有&nbsp;java&nbsp;提交中击败了98.53%的用户</div>\n<div class=\"css-fap0zx-ResultInfo e18r7j6f3\">内存消耗 :36.9 MB, 在所有&nbsp;java&nbsp;提交中击败了59.25%的用户</div>\n</div>\n<div class=\"css-fap0zx-ResultInfo e18r7j6f3\">&nbsp;</div>', '82. 删除排序链表中的重复元素 II', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 2, '2019-12-03 13:54:23', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (47, '46. 全排列', '<p>给定一个没有重复数字的序列，返回其所有可能的全排列。</p>\n<p>示例:</p>\n<p>输入: [1,2,3]<br />输出:<br />[<br />[1,2,3],<br />[1,3,2],<br />[2,1,3],<br />[2,3,1],<br />[3,1,2],<br />[3,2,1]<br />]</p>\n<hr />\n<div>思路：</div>\n<div>&nbsp;&nbsp; &nbsp;&nbsp; &nbsp; 回溯法，分别将（第一个，第二个...第nums.length-1个）元素和首个元素交换</div>\n<div>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp; &nbsp; 1{2,3,4}</div>\n<div>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp; &nbsp; 2{1,3,4}</div>\n<div>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp; &nbsp; 3{1,2,4}</div>\n<div>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp; &nbsp; 4{1,2,3}</div>\n<div>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp; &nbsp; &nbsp;&nbsp; &nbsp;递归执行{2,3,4}&nbsp;{1,3,4}&nbsp;{1,2,4}&nbsp;{1,2,3} 的全排序</div>\n<div>如图：</div>\n<div><img src=\"https://pic.leetcode-cn.com/0bf18f9b86a2542d1f6aa8db6cc45475fce5aa329a07ca02a9357c2ead81eec1-image.png\" alt=\"\" width=\"1000\" height=\"530\" /></div>\n<div>&nbsp; &nbsp;</div>\n<div>\n<pre class=\"language-java\"><code>class Solution {\n    public List&lt;List&lt;Integer&gt;&gt; permute(int[] nums) {\n        List&lt;List&lt;Integer&gt;&gt; res=new ArrayList&lt;&gt;();\n        backstack(nums,0,nums.length,res);\n        return res;\n    }\n\n    private void backstack(int[] nums,int begin,int end, List&lt;List&lt;Integer&gt;&gt; res){\n\n        if(begin==end){\n            List&lt;Integer&gt; tem = new ArrayList&lt;Integer&gt;();\n            for (int n = 0; n &lt; nums.length; n++) {\n                tem.add(nums[n]);\n            }\n            res.add(tem);\n            return;\n        }\n        for (int i = begin; i &lt;end; i++) {//begin-end 之间的全排列\n            swap(nums,i,begin);\n            backstack(nums,begin+1,end,res);\n            swap(nums,i,begin);\n\n        }\n    }\n\n    private  void swap(int[] nums, int x, int y) {\n        int a = nums[x];\n        nums[x] = nums[y];\n        nums[y] = a;\n    }\n}</code></pre>\n<div class=\"css-fap0zx-ResultInfo e18r7j6f3\"><hr /><img class=\"wscnph\" src=\"\" /></div>\n</div>', '46. 全排列', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 2, '2019-12-03 14:07:34', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (48, 'Java并发工具包中的Fork/Join框架', '<div>ForkJoin是由JDK1.7后提供多线并发处理框架。ForkJoin的框架的基本思想是分而治之。ForkJoin框架其实就是一个线程池ExecutorService的实现，通过工作窃取(work-stealing)算法,获取其他线程中未完成的任务来执行。可以充分利用机器的多处理器优势，利用空闲的线程去并行快速完成一个可拆分为小任务的大任务，类似于分治算法。ForkJoin的目标，就是利用所有可用的处理能力来提高程序的响应和性能。<br /><br /></div>\n<p><img src=\"https://upload-images.jianshu.io/upload_images/3047136-2044de505345b0a1.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp\" alt=\"\" width=\"1200\" height=\"536\" /></p>\n<h2>基础特性</h2>\n<p><img src=\"https://img-blog.csdnimg.cn/2019032123460544.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2NvZGluZ3R1,size_16,color_FFFFFF,t_70\" alt=\"\" width=\"948\" height=\"718\" /></p>\n<p>ForkJoin框架的核心是ForkJoinPool类，基于AbstractExecutorService扩展。ForkJoinPool中维护了一个队列数组WorkQueue[],每个WorkQueue维护一个ForkJoinTask数组和当前工作线程。ForkJoinPool实现了工作窃取(work-stealing)算法并执行ForkJoinTask。</p>\n<hr />\n<h2>案例使用</h2>\n<p>计算从1-100的和，使用Fork-Join分而治之</p>\n<pre class=\"language-java\"><code>public class ForkJoinDemo {\n    class CountRecursiveTask extends RecursiveTask&lt;Integer&gt; {\n        //达到子任务直接计算的阈值\n        private  final int Th = 10;\n\n        private int start;\n        private int end;\n\n        public CountRecursiveTask(int start, int end) {\n            this.start = start;\n            this.end = end;\n        }\n\n        @Override\n        protected Integer compute() {\n\n            if (end - start &lt;= Th) {\n                return count();//如果小于阈值，直接调用最小任务的计算方法\n            } else {  //如果仍大于阈值，则继续拆分为2个子任务，分别调用fork方法。\n                int middle = (end + start) / 2;\n                System.out.println(\"start:\" + start + \";middle:\" + middle + \";end:\" + end);\n                CountRecursiveTask left = new CountRecursiveTask(start, middle);//拆分左边\n                left.fork();\n                CountRecursiveTask right = new CountRecursiveTask(middle + 1, end);//拆分右边\n                right.fork();\n                return right.join() + left.join();\n            }\n\n\n        }\n\n        private int count() {\n            int sum = 0;\n            for (int i = start; i &lt;= end; i++) {\n                sum += i;\n            }\n            return sum;\n        }\n    }\n\n\n    public static void main(String[] args) {\n        ForkJoinPool forkJoinPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors());\n\n        Integer sum = forkJoinPool.invoke(new ForkJoinDemo().new CountRecursiveTask(1, 100));\n        System.out.println(sum);\n\n    }\n}</code></pre>\n<pre class=\"language-java\"><code>start:1;middle:50;end:100\nstart:1;middle:25;end:50\nstart:51;middle:75;end:100\nstart:1;middle:13;end:25\nstart:1;middle:7;end:13\nstart:14;middle:19;end:25\nstart:51;middle:63;end:75\nstart:51;middle:57;end:63\nstart:64;middle:69;end:75\nstart:76;middle:88;end:100\nstart:76;middle:82;end:88\nstart:89;middle:94;end:100\nstart:26;middle:38;end:50\nstart:26;middle:32;end:38\nstart:39;middle:44;end:50\n5050</code></pre>\n<pre class=\"language-markup\"><code>要注意的是两个任务都 fork 的情况，必须按照 f1.fork()，f2.fork()， f2.join()，f1.join() 这样的顺序，不然有性能问题。JDK官方文档有说明，有兴趣的可以去研究下。</code></pre>\n<p>有一点需要注意，Fork-Join框架只是在同时使用多核运行原来的单核程序，每个核心上的程序之间互不交叉。这不会改变算法原来的时间复杂度。比如查询一个超大数组中的最小值，复杂度为O(n),通过Fork-Join框架，将数组分为两部分，递归的查询每部分的最小值，这只是将原来的所有操作用两个核心运行，时间复杂度为O(n/2)，其实依然是O(n)。所以，假如排序一亿个数，单核用时30s，双核的用时也不会少于15s，因此，想要通过Fork-Join指数级提升性能是不现实的。但是对于已经成熟的系统，每一个微小部分的提升都有助于提升系统整体性能，从这个角度讲，Fork-Join还是有极大的用处。</p>\n<p>&nbsp;</p>\n<hr />\n<h2 id=\"2-1-工作顺序图\">工作顺序图</h2>\n<p>下图展示了以上代码的工作过程概要，但实际上Fork/Join框架的内部工作过程要比这张图复杂得多，例如如何决定某一个recursive task是使用哪条线程进行运行；再例如如何决定当一个任务/子任务提交到Fork/Join框架内部后，是创建一个新的线程去运行还是让它进行队列等待。</p>\n<p>所以如果不深入理解Fork/Join框架的运行原理，只是根据之上最简单的使用例子观察运行效果，那么我们只能知道子任务在Fork/Join框架中被拆分得足够小后，并且其内部使用多线程并行完成这些小任务的计算后再进行结果向上的合并动作，最终形成顶层结果。我们先从这张概要的过程图开始讨论。</p>\n<p><img src=\"https://img-blog.csdn.net/20170511170511140?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveWlud2Vuamll/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast\" alt=\"\" width=\"730\" height=\"520\" /></p>\n<p>图中最顶层的任务使用submit方式被提交到Fork/Join框架中，后者将前者放入到某个线程中运行，工作任务中的compute方法的代码开始对这个任务T1进行分析。如果当前任务需要累加的数字范围过大（代码中设定的是大于200），则将这个计算任务拆分成两个子任务（T1.1和T1.2），每个子任务各自负责计算一半的数据累加，请参见代码中的fork方法。如果当前子任务中需要累加的数字范围足够小（小于等于200），就进行累加然后返回到上层任务中。</p>\n<hr />\n<h2 id=\"2-2-forkjoinpool构造函数\">&nbsp;ForkJoinPool构造函数</h2>\n<p>ForkJoinPool有四个构造函数，其中参数最全的那个构造函数如下所示：</p>\n<pre class=\"language-java\"><code>public ForkJoinPool(int parallelism,\n                        ForkJoinWorkerThreadFactory factory,\n                        UncaughtExceptionHandler handler,\n                        boolean asyncMode)</code></pre>\n<ul>\n<li>\n<p>parallelism：可并行级别，Fork/Join框架将依据这个并行级别的设定，决定框架内并行执行的线程数量。并行的每一个任务都会有一个线程进行处理，但是千万不要将这个属性理解成Fork/Join框架中最多存在的线程数量，也不要将这个属性和ThreadPoolExecutor线程池中的corePoolSize、maximumPoolSize属性进行比较，因为ForkJoinPool的组织结构和工作方式与后者完全不一样。而后续的讨论中，读者还可以发现Fork/Join框架中可存在的线程数量和这个参数值的关系并不是绝对的关联（有依据但并不全由它决定）。</p>\n</li>\n<li>\n<p>factory：当Fork/Join框架创建一个新的线程时，同样会用到线程创建工厂。只不过这个线程工厂不再需要实现ThreadFactory接口，而是需要实现ForkJoinWorkerThreadFactory接口。后者是一个函数式接口，只需要实现一个名叫newThread的方法。在Fork/Join框架中有一个默认的ForkJoinWorkerThreadFactory接口实现：DefaultForkJoinWorkerThreadFactory。</p>\n</li>\n<li>\n<p>handler：异常捕获处理器。当执行的任务中出现异常，并从任务中被抛出时，就会被handler捕获。</p>\n</li>\n<li>\n<p>asyncMode：这个参数也非常重要，从字面意思来看是指的异步模式，它并不是说Fork/Join框架是采用同步模式还是采用异步模式工作。Fork/Join框架中为每一个独立工作的线程准备了对应的待执行任务队列，这个任务队列是使用数组进行组合的双向队列。即是说存在于队列中的待执行任务，即可以使用先进先出的工作模式，也可以使用后进先出的工作模式。（当asyncMode设置为ture的时候，队列采用先进先出方式工作；反之则是采用后进先出的方式工作，该值默认为false）</p>\n</li>\n</ul>\n<p>ForkJoinPool还有另外两个构造函数，一个构造函数只带有parallelism参数，既是可以设定Fork/Join框架的最大并行任务数量；另一个构造函数则不带有任何参数，对于最大并行任务数量也只是一个默认值&mdash;&mdash;当前操作系统可以使用的CPU内核数量（Runtime.getRuntime().availableProcessors()）。实际上ForkJoinPool还有一个私有的、原生构造函数，之上提到的三个构造函数都是对这个私有的、原生构造函数的调用。</p>\n<p>如果你对Fork/Join框架没有特定的执行要求，可以直接使用不带有任何参数的构造函数。也就是说推荐基于当前操作系统可以使用的CPU内核数作为Fork/Join框架内最大并行任务数量，这样可以保证CPU在处理并行任务时，尽量少发生任务线程间的运行状态切换（实际上单个CPU内核上的线程间状态切换基本上无法避免，因为操作系统同时运行多个线程和多个进程）。</p>\n<hr />\n<h2 id=\"2-3-fork方法和join方法\">&nbsp;fork方法和join方法</h2>\n<p>Fork/Join框架中提供的fork方法和join方法，可以说是该框架中提供的最重要的两个方法，它们和parallelism&ldquo;可并行任务数量&rdquo;配合工作，可以导致拆分的子任务T1.1、T1.2甚至TX在Fork/Join框架中不同的运行效果。例如TX子任务或等待其它已存在的线程运行关联的子任务，或在运行TX的线程中&ldquo;递归&rdquo;执行其它任务，又或者启动一个新的线程运行子任务&hellip;&hellip;</p>\n<p>fork方法用于将新创建的子任务放入当前线程的work queue队列中，Fork/Join框架将根据当前正在并发执行ForkJoinTask任务的ForkJoinWorkerThread线程状态，决定是让这个任务在队列中等待，还是创建一个新的ForkJoinWorkerThread线程运行它，又或者是唤起其它正在等待任务的ForkJoinWorkerThread线程运行它。</p>\n<p>这里面有几个元素概念需要注意，ForkJoinTask任务是一种能在Fork/Join框架中运行的特定任务，也只有这种类型的任务可以在Fork/Join框架中被拆分运行和合并运行。ForkJoinWorkerThread线程是一种在Fork/Join框架中运行的特性线程，它除了具有普通线程的特性外，最主要的特点是<strong>每一个ForkJoinWorkerThread线程都具有一个独立的任务等待队列（work queue）</strong>，这个任务队列用于存储在本线程中被拆分的若干子任务。</p>\n<p><img src=\"https://img-blog.csdn.net/20170514084721521?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveWlud2Vuamll/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast\" alt=\"\" width=\"673\" height=\"339\" /></p>\n<p>join方法用于让当前线程阻塞，直到对应的子任务完成运行并返回执行结果。或者，如果这个子任务存在于当前线程的任务等待队列（work queue）中，则取出这个子任务进行&ldquo;递归&rdquo;执行。其目的是尽快得到当前子任务的运行结果，然后继续执行。</p>\n<hr />\n<p><a href=\"https://blog.csdn.net/tyrroo/article/details/81390202\" target=\"_blank\" rel=\"noopener\">点击查看原文</a></p>', 'ForkJoin是由JDK1.7后提供多线并发处理框架。ForkJoin的框架的基本思想是分而治之。什么是分而治之？分而治之就是将一个复杂的计算，按照设定的阈值进行分解成多个计算，然后将各个计算结果进行汇总。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 1, '2019-12-07 10:46:25', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (49, 'JVM体系结构', '<h1>JVM体系结构图：</h1>\n<p><img src=\"https://upload-images.jianshu.io/upload_images/6423761-0e92ccf1175a7962.png?imageMogr2/auto-orient/strip|imageView2/2/w/859/format/webp\" alt=\"\" width=\"859\" height=\"738\" /></p>\n<div>\n<h1>JVM字节码文件（JVM Languages Classes ）</h1>\n<p>包括但不仅限于Java语言编译而成的Class文件。实际上，Java虚拟机不和包括Java在内的任何编程语言绑定，它只与&ldquo;Class文件&rdquo;这种特定的二进制文件格式关联，只要特定语言的编译器能将代码编译成Class文件，虚拟机并不关心Class的来源是何种语言，如下图所示</p>\n<p><img src=\"https://upload-images.jianshu.io/upload_images/6423761-6a635c8b3dab4725.png?imageMogr2/auto-orient/strip|imageView2/2/w/854/format/webp\" alt=\"\" width=\"854\" height=\"426\" /></p>\n<div>\n<h1>类加载器（Class Loader）</h1>\n<p>Class文件需要被加载到内存里才能得以运行和使用。虚拟机把Class文件加载到内存后，对数据进行验证、转换解析和初始化，最终形成可以被虚拟机直接使用的Java类型，这就是虚拟机的类加载机制。具体内容，我们将在之后的篇幅进行详细一点的介绍。</p>\n<h1>运行时数据区（Runtime Data Area）</h1>\n<p>Java虚拟机在运行程序的过程中会把它所管理的内存划分为若干个不同的数据区，上图&ldquo;JVM Architecture&rdquo;中，基于内存是否能被线程所共享，内存被分为了蓝色和白色两大块区域，蓝色区域表示所有线程都会向此区域读写数据，白色区域表示这些区域是线程私有的，每条线程都有自己的虚拟机栈、本地方法栈、程序计数器，各条线程之间的栈和计数器相互隔离。它们之间的关系可以表示为下图：</p>\n<p><img src=\"https://upload-images.jianshu.io/upload_images/6423761-c44a15402a52ad80.png?imageMogr2/auto-orient/strip|imageView2/2/w/769/format/webp\" alt=\"\" width=\"769\" height=\"465\" /></p>\n<p>各数据区的作用将在下一个章节开始详细介绍。</p>\n<h1>执行引擎（Execution Engine）</h1>\n<p>执行字节码指令，该区域包括解释器、编译器和垃圾回收器</p>\n<p><img src=\"https://upload-images.jianshu.io/upload_images/6423761-8c71267d38fa1fc2.png?imageMogr2/auto-orient/strip|imageView2/2/w/489/format/webp\" alt=\"\" width=\"489\" height=\"244\" /></p>\n<div>\n<ul>\n<li>解释器：解释器更快地解释字节码，但执行缓慢。解释器的缺点是当一个方法被调用多次时，每次都需要一个新的解释</li>\n<li>JIT编译器：JIT编译器消除了解释器的缺点。执行引擎将在转换字节码时使用解释器的帮助，但是当它发现重复的代码时，将使用JIT编译器，它编译整个字节码并将其更改为本地代码。这个本地代码将直接用于重复的方法调用，这提高了系统的性能</li>\n<li>垃圾收集器：收集和删除未引用的对象，来释放内存空间。</li>\n</ul>\n<div>\n<div>\n<h1>本地库接口（Native Interface）</h1>\n<p>提供一个标准的方式让Java程序通过虚拟机与原生代码进行交互，这也就是我们平常常说的Java本地接口（JNI&mdash;&mdash;Java Native Interface）。它使得在 JVM 内部运行的Java 代码能够与用其它编程语言（如 C、C++ 和汇编语言）编写的应用程序和库进行互操作。JNI最重要的好处是它没有对底层 Java 虚拟机的实现施加任何限制。因此，Java虚拟机厂商可以在不影响虚拟机其它部分的情况下添加对JNI的支持。程序员只需编写一种版本的本地应用程序或库，就能够与所有支持JNI 的Java 虚拟机协同工作。</p>\n<h1>本地方法库（Native Libraires）</h1>\n<p>它是执行引擎所需的本机库的集合</p>\n<hr />\n<p><a href=\"https://www.jianshu.com/p/33948336306f\" target=\"_blank\" rel=\"noopener\">点击查看原文</a></p>\n</div>\n</div>\n</div>\n</div>\n</div>', 'JVM全称Java Virtual Machine（Java虚拟机），是一个虚构出来的计算机，它屏蔽了与具体操作系统平台相关的信息，使得Java程序只需生成在Java虚拟机上运行的目标代码（字节码，ByteCode）, 就可以在多种平台上不加修改地运行。这背后其实就是JVM把字节码翻译成具体平台上的机器指令，从而实现“一次编写，到处运行（Write Once, Run Anywhere）”', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 5, '2019-12-07 11:46:47', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (50, 'JVM的三种类加载器和双亲委派机制', '<p>JVM中的类的加载器主要有三种:启动类加载器，拓展类加载器，应用类加载器。</p>\n<p>&nbsp; &nbsp; &nbsp;启动类加载器(Bootstrap classLoader):又称为引导类加载器，由C++编写，无法通过程序得到。主要负责加载JAVA中的 一些核心类库，主要是位于&lt;JAVA_HOME&gt;/lib/rt.jar中。</p>\n<p>&nbsp; &nbsp; &nbsp;拓展类加载器(Extension classLoader):主要加载JAVA中的一些拓展类，位于&lt;JAVA_HOME&gt;/lib/ext中,是启动类加载器的子类。</p>\n<p>&nbsp; &nbsp; &nbsp;应用类加载器(System classLoader): 又称为系统类加载器,主要用于加载CLASSPATH路径下我们自己写的类，是拓展类加载器的子类。<br /><br /></p>\n<p><img src=\"https://oscimg.oschina.net/oscnet/5c3578c7229c3a17ce647596563bb95a351.jpg\" alt=\"\" width=\"625\" height=\"458\" /></p>\n<h2>验证：</h2>\n<pre class=\"language-java\"><code>public class Hello {\n    public static void main(String[] args) {\n        Object object=new Object();//系统类 位于rt.jar中\n        Hello hello=new Hello();// 我们自定义的类\n        System.out.println(object.getClass().getClassLoader());\n        System.out.println(hello.getClass().getClassLoader());\n        System.out.println(hello.getClass().getClassLoader().getParent());\n        System.out.println(hello.getClass().getClassLoader().getParent().getParent());\n    }\n}</code></pre>\n<pre class=\"language-java\"><code>null\nsun.misc.Launcher$AppClassLoader@18b4aac2\nsun.misc.Launcher$ExtClassLoader@1540e19d\nnull</code></pre>\n<p>.getClassLoader().getParent().getParent()与object.getClass().getClassLoader()得到将会是Null，因为启动类（Bootstrap classLoader)）加载器是用C++写的，我们无法通过程序直接得到.</p>\n<hr />\n<h1>双亲委派</h1>\n<p>双亲委派的意思是如果一个类加载器需要加载类，那么首先它会把这个类请求委派给父类加载器去完成，每一层都是如此。一直递归到顶层，当父加载器无法完成这个请求时，子类才会尝试去加载。</p>\n<h2><span class=\"bjh-p\">双亲委派有啥好处呢？</span></h2>\n<p><span class=\"bjh-p\">它使得类有了层次的划分。就拿java.lang.Object来说，你加载它经过一层层委托最终是由Bootstrap ClassLoader来加载的，也就是最终都是由Bootstrap ClassLoader去找&lt;JAVA_HOME&gt;\\lib中rt.jar里面的java.lang.Object加载到JVM中。</span></p>\n<p><span class=\"bjh-p\">这样如果有不法分子自己造了个java.lang.Object,里面嵌了不好的代码，如果我们是按照双亲委派模型来实现的话，最终加载到JVM中的只会是我们rt.jar里面的东西，也就是这些核心的基础类代码得到了保护。因为这个机制使得系统中只会出现一个java.lang.Object。不会乱套了。你想想如果我们JVM里面有两个Object,那岂不是天下大乱了。</span></p>', '  JVM中的类的加载器主要有三种:启动类加载器，拓展类加载器，应用类加载器。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 5, '2019-12-07 15:57:36', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (51, 'JVM栈(stack)', '<h1>定义</h1>\n<p>每个JVM线程拥有一个私有的 Java虚拟机栈，创建线程的同时栈也被创建。一个JVM栈由许多帧组成，称之为<span class=\"hljs-string\">\"栈帧\"</span>。JVM中的栈和<span class=\"hljs-keyword\">C</span>等常见语言中的栈比较类似，都用于保存局部变量和部分计算结果，同时也参与方法调用和返回。</p>\n<p>对于栈来说不存在垃圾回收问题。只要线程一结束该栈则Over，生命周期和所在线程一致，是线程私有。</p>\n<p><span style=\"color: #ff0000;\">8中基本类型变量+对象的引用变量+实例方法都是在函数栈内存分配。</span></p>\n<h2>栈存储什么？</h2>\n<p>本地变量（Local Variables）：输入参数和输出参数以及方法内的变量；</p>\n<p>栈操作（Operand Stack）：记录出栈，入栈的操作；</p>\n<p>栈帧数据（Frame Data）：包括类文件，方法等；</p>\n<h1>栈结构</h1>\n<p>栈帧（Stack Frame）是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区中的java虚拟机栈的栈元素。</p>\n<p>栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。</p>\n<p><img src=\"https://img2018.cnblogs.com/blog/858186/201907/858186-20190711154743556-950500902.png\" alt=\"\" /></p>\n<h2>局部变量表</h2>\n<p>1.局部变量表（Local Variable Table）是一组变量值存储空间，用于存放方法参数和方法内部定义的局部变量。&nbsp;并且在Java编译为Class文件时，就已经确定了该方法所需要分配的局部变量表的最大容量。</p>\n<p>2.局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)「String是引用类型」，对象引用(reference类型) 和 returnAddress类型（它指向了一条字节码指令的地址）</p>\n<hr />\n<p>　注意：</p>\n<p>　　很多人说：基本数据和对象引用存储在栈中。</p>\n<p>　　当然这种说法虽然是正确的，但是很不严谨，只能说这种说法针对的是局部变量。</p>\n<p>　　局部变量存储在局部变量表中，随着线程而生，线程而灭。并且线程间数据不共享。</p>\n<p>　　但是，如果是成员变量，或者定义在方法外对象的引用，它们存储在堆中。</p>\n<p>　　因为在堆中，是线程共享数据的，并且栈帧里的命名就已经清楚的划分了界限 : 局部变量表！</p>\n<hr />\n<h2>操作数栈</h2>\n<ul>\n<li>与局部变量表一样，均以字长为单位的数组。不过局部变量表用的是索引，操作数栈是弹栈/压栈来访问。操作数栈可理解为java虚拟机栈中的一个用于计算的临时数据存储区。</li>\n<li>存储的数据与局部变量表一致含int、long、float、double、reference、returnType，操作数栈中byte、short、char压栈前(bipush)会被转为int。</li>\n<li>数据运算的地方，大多数指令都在操作数栈弹栈运算，然后结果压栈。</li>\n<li>java虚拟机栈是方法调用和执行的空间，每个方法会封装成一个栈帧压入占中。其中里面的操作数栈用于进行运算，当前线程只有当前执行的方法才会在操作数栈中调用指令（可见java虚拟机栈的指令主要取于操作数栈）。</li>\n<li>如果int类型在-128~127(这个就直接存在常量池了)</li>\n</ul>\n<h2>动态连接</h2>\n<p>　　每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用，</p>\n<p>　　持有这个引用是为了支持方法调用过程中的动态连接（Dynamic Linking）。</p>\n<p>　　在类加载阶段中的解析阶段会将符号引用转为直接引用，这种转化也称为静态解析。</p>\n<p>　　另外的一部分将在每一次运行时期转化为直接引用。这部分称为动态连接。</p>\n<p>　　这里简单提一下动态连接的概念,后面在详细讲解.</p>\n<h2>方法出口</h2>\n<p>　　当一个方法开始执行后，只有2种方式可以退出这个方法 ：</p>\n<p>　　方法返回指令&nbsp;： 执行引擎遇到一个方法返回的字节码指令，这时候有可能会有返回值传递给上层的方法调用者，这种退出方式称为正常完成出口。</p>\n<p>　　异常退出&nbsp;： 在方法执行过程中遇到了异常，并且没有处理这个异常，就会导致方法退出。</p>\n<p>　　无论采用任何退出方式，在方法退出之后，都需要返回到方法被调用的位置，程序才能继续执行，方法返回时可能需要在栈帧中保存一些信息。</p>\n<p>　　　　一般来说，方法正常退出时，调用者的PC计数器的值可以作为返回地址，栈帧中会保存这个计数器值。</p>\n<p>　　而方法异常退出时，返回地址是要通过异常处理器表来确定的，栈帧中一般不会保存这部分信息。</p>', '每个JVM线程拥有一个私有的 Java虚拟机栈，创建线程的同时栈也被创建。一个JVM栈由许多帧组成，称之为\"栈帧\"。JVM中的栈和C等常见语言中的栈比较类似，都用于保存局部变量和部分计算结果，同时也参与方法调用和返回。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 5, '2019-12-07 18:52:11', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (52, 'JVM堆内存(heap)', '<p>JAVA堆内存管理是影响性能主要因素之一。<br />堆内存溢出是JAVA项目非常常见的故障，在解决该问题之前，必须先了解下JAVA堆内存是怎么工作的。</p>\n<p>先看下JAVA堆内存是如何划分的，如图：</p>\n<p><img src=\"http://i2.51cto.com/images/blog/201808/21/b116170771ecb3117ae7fead03fcaa0d.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=\" alt=\"\" /></p>\n<ol>\n<li>JVM内存划分为堆内存和非堆内存，堆内存分为年轻代（Young Generation）、老年代（Old Generation），非堆内存就一个永久代（Permanent Generation）。</li>\n<li>年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量，Survivor两个区占小容量，默认比例是8:1:1。</li>\n<li>堆内存用途：存放的是对象，垃圾收集器就是收集这些对象，然后根据GC算法回收。</li>\n<li>非堆内存用途：永久代，也称为方法区，存储程序运行时长期存活的对象，比如类的元数据、方法、常量、属性等。</li>\n</ol>\n<p>在JDK1.8版本废弃了永久代，替代的是元空间（MetaSpace），元空间与永久代上类似，都是方法区的实现，他们最大区别是：元空间并不在JVM中，而是使用本地内存。<br />元空间有注意有两个参数：</p>\n<ul>\n<li>MetaspaceSize ：初始化元空间大小，控制发生GC阈值</li>\n<li>MaxMetaspaceSize ： 限制元空间大小上限，防止异常占用过多物理内存</li>\n</ul>\n<h2>为什么移除永久代？</h2>\n<p>移除永久代原因：为融合HotSpot JVM与JRockit VM（新JVM技术）而做出的改变，因为JRockit没有永久代。<br />有了元空间就不再会出现永久代OOM问题了！</p>\n<h2>分代概念</h2>\n<p>新生成的对象首先放到年轻代Eden区，当Eden空间满了，触发Minor GC，存活下来的对象移动到Survivor0区，Survivor0区满后触发执行Minor GC，Survivor0区存活对象移动到Suvivor1区，这样保证了一段时间内总有一个survivor区为空。经过多次Minor GC仍然存活的对象移动到老年代。<br />老年代存储长期存活的对象，占满时会触发Major GC=Full GC，GC期间会停止所有线程等待GC完成，所以对响应要求高的应用尽量减少发生Major GC，避免响应超时。<br />Minor GC ： 清理年轻代&nbsp;<br />Major GC ： 清理老年代<br />Full GC ： 清理整个堆空间，包括年轻代和永久代<br />所有GC都会停止应用所有线程。</p>\n<h2>为什么分代？</h2>\n<p>将对象根据存活概率进行分类，对存活时间长的对象，放到固定区，从而减少扫描垃圾时间及GC频率。针对分类进行不同的垃圾回收算法，对算法扬长避短。</p>\n<h2>为什么survivor分为两块相等大小的幸存空间？</h2>\n<p>主要为了解决碎片化。如果内存碎片化严重，也就是两个对象占用不连续的内存，已有的连续内存不够新对象存放，就会触发GC。</p>\n<h2>JVM堆内存常用参数</h2>\n<div class=\"table-box\">\n<table>\n<thead>\n<tr>\n<th>参数</th>\n<th>描述</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>-Xms</td>\n<td>堆内存初始大小，单位m、g</td>\n</tr>\n<tr>\n<td>-Xmx（MaxHeapSize）</td>\n<td>堆内存最大允许大小，一般不要大于物理内存的80%</td>\n</tr>\n<tr>\n<td>-XX:PermSize</td>\n<td>非堆内存初始大小，一般应用设置初始化200m，最大1024m就够了</td>\n</tr>\n<tr>\n<td>-XX:MaxPermSize</td>\n<td>非堆内存最大允许大小</td>\n</tr>\n<tr>\n<td>-XX:NewSize（-Xns）</td>\n<td>年轻代内存初始大小</td>\n</tr>\n<tr>\n<td>-XX:MaxNewSize（-Xmn）</td>\n<td>年轻代内存最大允许大小，也可以缩写</td>\n</tr>\n<tr>\n<td>-XX:SurvivorRatio=8</td>\n<td>年轻代中Eden区与Survivor区的容量比例值，默认为8，即8:1</td>\n</tr>\n<tr>\n<td>-Xss</td>\n<td>堆栈内存大小</td>\n</tr>\n</tbody>\n</table>\n<p>&nbsp;</p>\n<h2>垃圾回收算法（GC，Garbage Collection）</h2>\n<p>红色是标记的非活动对象，绿色是活动对象。</p>\n<ul>\n<li><strong>标记-清除（Mark-Sweep）</strong><br />GC分为两个阶段，标记和清除。首先标记所有可回收的对象，在标记完成后统一回收所有被标记的对象。同时会产生不连续的内存碎片。碎片过多会导致以后程序运行时需要分配较大对象时，无法找到足够的连续内存，而不得已再次触发GC。</li>\n</ul>\n<p style=\"padding-left: 40px;\"><img src=\"http://i2.51cto.com/images/blog/201808/21/248dbe09bbf26285cba1760ad0355faa.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=\" alt=\"\" /></p>\n<ul>\n<li><strong>复制（Copy）</strong><br />将内存按容量划分为两块，每次只使用其中一块。当这一块内存用完了，就将存活的对象复制到另一块上，然后再把已使用的内存空间一次清理掉。这样使得每次都是对半个内存区回收，也不用考虑内存碎片问题，简单高效。缺点需要两倍的内存空间。</li>\n</ul>\n<p style=\"padding-left: 40px;\"><img src=\"http://i2.51cto.com/images/blog/201808/21/55b07a315eefb27aab131363cdf328ec.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=\" alt=\"\" /></p>\n<ul>\n<li><strong>标记-整理（Mark-Compact）</strong><br />也分为两个阶段，首先标记可回收的对象，再将存活的对象都向一端移动，然后清理掉边界以外的内存。此方法避免标记-清除算法的碎片问题，同时也避免了复制算法的空间问题。<br />一般年轻代中执行GC后，会有少量的对象存活，就会选用复制算法，只要付出少量的存活对象复制成本就可以完成收集。而老年代中因为对象存活率高，没有额外过多内存空间分配，就需要使用标记-清理或者标记-整理算法来进行回收。</li>\n</ul>\n<p style=\"padding-left: 40px;\"><img src=\"http://i2.51cto.com/images/blog/201808/21/70a593b26725157302078821fec7f37d.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=\" alt=\"\" /></p>\n<h2>为什么会堆内存溢出？</h2>\n<p>在年轻代中经过GC后还存活的对象会被复制到老年代中。当老年代空间不足时，JVM会对老年代进行完全的垃圾回收（Full GC）。如果GC后，还是无法存放从Survivor区复制过来的对象，就会出现OOM（Out of Memory）。</p>\n<p><strong>OOM（Out of Memory）异常常见有以下几个原因：</strong><br />1）老年代内存不足：java.lang.OutOfMemoryError:Javaheapspace<br />2）永久代内存不足：java.lang.OutOfMemoryError:PermGenspace<br />3）代码bug，占用内存无法及时回收。<br />OOM在这几个内存区都有可能出现，实际遇到OOM时，能根据异常信息定位到哪个区的内存溢出。<br />可以通过添加个参数-XX:+HeapDumpOnOutMemoryError，让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后分析。</p>\n<p>熟悉了JAVA内存管理机制及配置参数，下面是对JAVA应用启动选项调优配置：</p>\n<pre class=\"has\"><code class=\"hljs php\"></code></pre>\n<ol class=\"hljs-ln\">\n<li>\n<div class=\"hljs-ln-numbers\">\n<div class=\"hljs-ln-line hljs-ln-n\" data-line-number=\"1\">&nbsp;</div>\n</div>\n<div class=\"hljs-ln-code\">\n<div class=\"hljs-ln-line\">JAVA_OPTS=<span class=\"hljs-string\">\"-server -Xms512m -Xmx2g -XX:+UseG1GC -XX:SurvivorRatio=6 -XX:MaxGCPauseMillis=400 -XX:G1ReservePercent=15 -XX:ParallelGCThreads=4 -XX:</span></div>\n</div>\n</li>\n<li>\n<div class=\"hljs-ln-numbers\">\n<div class=\"hljs-ln-line hljs-ln-n\" data-line-number=\"2\">&nbsp;</div>\n</div>\n<div class=\"hljs-ln-code\">\n<div class=\"hljs-ln-line\"><span class=\"hljs-string\">ConcGCThreads=1 -XX:InitiatingHeapOccupancyPercent=40 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:../logs/gc.log\"</span></div>\n</div>\n</li>\n</ol>\n<pre class=\"has\"><code class=\"hljs php\"></code></pre>\n<ul>\n<li>设置堆内存最小和最大值，最大值参考历史利用率设置</li>\n<li>设置GC垃圾收集器为G1</li>\n<li>启用GC日志，方便后期分析</li>\n</ul>\n<h2>小结</h2>\n<ul>\n<li>选择高效的GC算法，可有效减少停止应用线程时间。</li>\n<li>频繁Full GC会增加暂停时间和CPU使用率，可以加大老年代空间大小降低Full GC，但会增加回收时间，根据业务适当取舍。</li>\n</ul>\n<hr />\n<p><a href=\"https://blog.csdn.net/lingbo229/article/details/82586822\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>\n</div>', '“Java 虚拟机具有一个堆(Heap)，堆是运行时数据区域，所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。”“在JVM中堆之外的内存称为非堆内存(Non-heap memory)”。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 5, '2019-12-07 19:32:24', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (53, '快速排序和三路快排', '<h1>快速排序</h1>\n<div class=\"para\">快速排序算法通过多次比较和交换来实现排序，其排序流程如下：</div>\n<div class=\"para\">(1)首先设定一个分界值，通过该分界值将数组分成左右两部分。</div>\n<div class=\"para\">(2)将大于或等于分界值的数据集中到数组右边，小于分界值的数据集中到数组的左边。此时，左边部分中各元素都小于或等于分界值，而右边部分中各元素都大于或等于分界值。</div>\n<div class=\"para\">(3)然后，左边和右边的数据可以独立排序。对于左侧的数组数据，又可以取一个分界值，将该部分数据分成左右两部分，同样在左边放置较小值，右边放置较大值。右侧的数组数据也可以做类似处理。</div>\n<div class=\"para\">(4)重复上述过程，可以看出，这是一个递归定义。通过递归将左侧部分排好序后，再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后，整个数组的排序也就完成了。</div>\n<pre class=\"language-java\"><code>public class quicksort {\n    public static void sort(int[] arry,int lo,int lt)\n    {\n        if(lo&lt;lt){\n        int t=partition(arry,lo,lt);\n        sort(arry,lo,t-1);\n        sort(arry,t+1,lt);\n        }\n\n    }\n\n    //快速排序的切分\n    private static int partition(int[] arry, int lo, int hi) {\n        int i=lo,j=hi+1;//左右扫描指针\n        int x=arry[lo];//切分元素\n        while(true){\n            //扫描左右，检查是否结束并交换元素\n            while(arry[++i]&lt;x){if(i==hi)break;}\n            while(arry[--j]&gt;x){if(j==lo)break;}\n            if(i&gt;=j)break;\n            swap(arry,i,j);\n\n        }\n        swap(arry,lo,j);//将x=arry[lo]放入正确的位置\n        return j;//a[lo..j-1]&lt;=a[j]&lt;=a[j+1..hi]达成\n    }\n\n    private static void swap(int[] arry, int i, int j) {\n        int x;\n        x=arry[j];\n        arry[j]=arry[i];\n        arry[i]=x;\n    }\n}</code></pre>\n<hr />\n<h1>三路快排</h1>\n<p>但是若序列中包含大量重复的元素，这种情况下，快排的性能就不那么理想了。</p>\n<p>下面开始学习三路快排的思想吧。</p>\n<h2>思路</h2>\n<p>将数组分为三部分，分别对应于小于、等于和大于哨兵元素tem的子序列。</p>\n<pre class=\"language-java\"><code>lo~le-1：未排序\nle~i-1：等于tem\ni~lt：未排序\nlt+1~hi:大于tem</code></pre>\n<pre class=\"language-java\"><code>    public void sort(int[] arr) {\n        backStack(arr, 0, arr.length-1);\n    }\n\n    public void backStack(int[] arr, int lo, int hi) {\n        if (lo &gt;= hi) {\n            return;\n        }\n        int i = lo + 1;\n        int le = lo;// lo-le-1 &lt;tem\n        int lt = hi;// lt-hi-1 &gt;tem\n        int tem = arr[lo];\n        while (i &lt;= lt) {\n            if (arr[i] &gt; tem) {\n                swap(arr, i, lt--);\n            } else if (arr[i] &lt; tem) {\n                swap(arr, i++, le++);\n            } else {\n                i++;\n            }\n        }\n        backStack(arr, lo, le - 1);\n        backStack(arr, lt + 1, hi);\n\n    }</code></pre>\n<p>&nbsp;</p>', '快速排序可以说是20世纪最伟大的算法之一了。相信都有所耳闻，它的速度也正如它的名字那样，是一个非常快的算法了。当然它也后期经过了不断的改进和优化，才被公认为是一个值得信任的非常优秀的算法。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 3, '2019-12-08 12:33:31', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (54, '96. 不同的二叉搜索树', '<p>给定一个整数 n，求以&nbsp;1 ...&nbsp;n&nbsp;为节点组成的二叉搜索树有多少种？</p>\n<p>示例:</p>\n<p>输入: 3<br />输出: 5<br />解释:<br />给定 n = 3, 一共有 5 种不同结构的二叉搜索树:</p>\n<pre><img src=\"https://upload-images.jianshu.io/upload_images/5679451-7158f8ce8af4d528.png?imageMogr2/auto-orient/strip|imageView2/2/w/434/format/webp\" alt=\"\" width=\"434\" height=\"136\" /></pre>\n<hr />\n<pre>题解参考的官方答案</pre>\n<h3>动态规划<br />直觉</h3>\n<p>本问题可以用动态规划求解。</p>\n<p>给定一个有序序列 1 ... n，为了根据序列构建一棵二叉搜索树。我们可以遍历每个数字 i，将该数字作为树根，1 ... (i-1) 序列将成为左子树，(i+1) ... n 序列将成为右子树。于是，我们可以递归地从子序列构建子树。<br />在上述方法中，由于根各自不同，每棵二叉树都保证是独特的。</p>\n<p>可见，问题可以分解成规模较小的子问题。因此，我们可以存储并复用子问题的解，而不是递归的（也重复的）解决这些子问题，这就是动态规划法。</p>\n<p><strong>算法</strong></p>\n<p>问题是计算不同二叉搜索树的个数。为此，我们可以定义两个函数：</p>\n<ol>\n<li>\n<p><span class=\"katex\"><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"mord mathdefault\">G</span><span class=\"mopen\">(</span><span class=\"mord mathdefault\">n</span><span class=\"mclose\">)</span></span></span></span>: 长度为<code>n</code>的序列的不同二叉搜索树个数。</p>\n</li>\n<li>\n<p><span class=\"katex\"><span class=\"katex-mathml\">F(i, n)</span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"mord mathdefault\">F</span><span class=\"mopen\">(</span><span class=\"mord mathdefault\">i</span><span class=\"mpunct\">,</span><span class=\"mord mathdefault\">n</span><span class=\"mclose\">)</span></span></span></span>: 以i为根的不同二叉搜索树个数(<span class=\"katex\"><span class=\"katex-mathml\">1 &lt;= i &lt;=n</span></span>)。</p>\n</li>\n</ol>\n<p>可见，</p>\n<blockquote>\n<p><span class=\"katex\"><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"mord mathdefault\">G</span><span class=\"mopen\">(</span><span class=\"mord mathdefault\">n</span><span class=\"mclose\">)</span></span></span></span>&nbsp;是我们解决问题需要的函数。</p>\n</blockquote>\n<p>稍后我们将看到，G(n) 可以从 F(i, n) 得到，而 F(i, n)又会递归的依赖于G(n)。</p>\n<p>首先，根据上一节中的思路，不同的二叉搜索树的总数 G(n)，是对遍历所有 i (1 &lt;= i &lt;= n) 的 F(i, n) 之和。换而言之：</p>\n<p><img class=\"wscnph\" src=\"\" /></p>\n<p>特别的，对于边界情况，当序列长度为 1 （只有根）或为 0 （空树）时，只有一种情况。亦即：</p>\n<p><span class=\"base\"><span class=\"mord mathdefault\">G</span><span class=\"mopen\">(</span><span class=\"mord\">0</span><span class=\"mclose\">)</span><span class=\"mrel\">=</span></span><span class=\"base\"><span class=\"mord\">1</span><span class=\"mpunct\">,</span><span class=\"mord mathdefault\">G</span><span class=\"mopen\">(</span><span class=\"mord\">1</span><span class=\"mclose\">)</span><span class=\"mrel\">=</span></span><span class=\"base\"><span class=\"mord\">1</span></span></p>\n<p><span class=\"base\">给定序列&nbsp;<code>1 ... n</code>，我们选出数字&nbsp;<code>i</code>&nbsp;作为根，则对于根 i 的不同二叉搜索树数量&nbsp;<span class=\"katex\"><span class=\"katex-mathml\">F(i, n)</span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"mord mathdefault\">F</span><span class=\"mopen\">(</span><span class=\"mord mathdefault\">i</span><span class=\"mpunct\">,</span><span class=\"mord mathdefault\">n</span><span class=\"mclose\">)</span></span></span>，是左右子树个数的<strong>笛卡尔积</strong>，如下图所示:</span></p>\n<p><span class=\"base\"><img src=\"https://pic.leetcode-cn.com/fe9fb329250b328bb66032dda25b867e0047fcb480c2c0bcf14ecc2a4c12e454-image.png\" alt=\"\" width=\"700\" /></span></p>\n<p>举例而言，F(3, 7)F(3,7)，以 3 为根的不同二叉搜索树个数。为了以 3 为根从序列 [1, 2, 3, 4, 5, 6, 7] 构建二叉搜索树，我们需要从左子序列 [1, 2] 构建左子树，从右子序列 [4, 5, 6, 7] 构建右子树，然后将它们组合(即笛卡尔积)。<br />巧妙之处在于，我们可以将 [1,2] 构建不同左子树的数量表示为 G(2)G(2), 从 [4, 5, 6, 7]` 构建不同右子树的数量表示为 G(4)G(4)。这是由于 G(n)G(n) 和序列的内容无关，只和序列的长度有关。于是，F(3,7)=G(2)*G(4)。 概括而言，我们可以得到以下公式：</p>\n<p><img class=\"wscnph\" src=\"\" /></p>\n<p>将公式 (1)，(2) 结合，可以得到&nbsp;<span class=\"katex\"><span class=\"katex-mathml\">G(n)</span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"mord mathdefault\">G</span><span class=\"mopen\">(</span><span class=\"mord mathdefault\">n</span><span class=\"mclose\">)</span></span></span></span>&nbsp;的递归表达公式：</p>\n<p><img class=\"wscnph\" src=\"\" /></p>\n<p>为了计算函数结果，我们从小到大计算，因为 G(n) 的值依赖于G(0)&hellip;G(n&minus;1)。</p>\n<p>根据以上的分析和公式，很容易实现计算 G(n) 的算法。 下面是示例:</p>\n<pre class=\"language-java\"><code>public class Solution {\n  public int numTrees(int n) {\n    int[] G = new int[n + 1];\n    G[0] = 1;\n    G[1] = 1;\n\n    for (int i = 2; i &lt;= n; ++i) {\n      for (int j = 1; j &lt;= i; ++j) {\n        G[i] += G[j - 1] * G[i - j];\n      }\n    }\n    return G[n];\n  }\n}</code></pre>\n<p><img class=\"wscnph\" src=\"\" /></p>\n<hr />\n<p><a href=\"https://leetcode-cn.com/problems/unique-binary-search-trees/solution/bu-tong-de-er-cha-sou-suo-shu-by-leetcode/\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>', '96. 不同的二叉搜索树', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 2, '2019-12-08 21:47:28', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (55, 'BIO,NIO,AIO区别', '<h2>BIO</h2>\n<p>Java BIO即Block I/O ， 同步并阻塞的IO。</p>\n<p>BIO 就是阻塞IO，每个TCP连接进来服务端都需要创建一个线程来建立连接并进行消息的处理。如果中间发生了阻塞(比如建立连接、读数据、写数据时发生阻碍)，线程也会发生阻塞，并发情况下，N个连接需要N个线程来处理。</p>\n<p><img src=\"https://img-blog.csdn.net/20170226131634860?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxMDg1MzI2MQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast\" alt=\"\" width=\"1192\" height=\"514\" /></p>\n<h2>NIO</h2>\n<p>NIO是JDK1.4提出的，还是先用一段通俗的话来说明NIO的工作原理：</p>\n<p>NIO 也就是非阻塞IO，是基于事件驱动的思想(Reactor线程模型)。对比与BIO来说，NIO使用一个线程来管理所有的Socket 通道，也就是基于Selector机制，当查询到事件时(连接、接受连接、读、写)，就会转发给不同的处理线程(handler)。</p>\n<p>下面给出Reactor模型的工作应用图：</p>\n<p><img src=\"https://img-blog.csdn.net/20170226131734232?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxMDg1MzI2MQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast\" alt=\"\" width=\"988\" height=\"488\" /></p>\n<h2>AIO</h2>\n<p>Java AIO即Async非阻塞，是异步非阻塞的IO。AIO是JDK1.7提出的，也就是异步IO</p>\n<p><a href=\"https://blog.csdn.net/u010853261/article/details/57409540\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>\n<hr />\n<h2>区别及联系</h2>\n<ul>\n<li>BIO （Blocking I/O）：同步阻塞I/O模式，数据的读取写入必须阻塞在一个线程内等待其完成。这里假设一个烧开水的场景，有一排水壶在烧开水，BIO的工作模式就是， 叫一个线程停留在一个水壶那，直到这个水壶烧开，才去处理下一个水壶。但是实际上线程在等待水壶烧开的时间段什么都没有做。</li>\n<li>NIO （New I/O）：同时支持阻塞与非阻塞模式，但这里我们以其同步非阻塞I/O模式来说明，那么什么叫做同步非阻塞？如果还拿烧开水来说，NIO的做法是叫一个线程不断的轮询每个水壶的状态，看看是否有水壶的状态发生了改变，从而进行下一步的操作。</li>\n<li>AIO （ Asynchronous I/O）：异步非阻塞I/O模型。异步非阻塞与同步非阻塞的区别在哪里？异步非阻塞无需一个线程去轮询所有IO操作的状态改变，在相应的状态改变后，系统会通知对应的线程来处理。对应到烧开水中就是，为每个水壶上面装了一个开关，水烧开之后，水壶会自动通知我水烧开了。</li>\n</ul>\n<h2>各自适用场景</h2>\n<ul>\n<li>BIO方式适用于连接数目比较小且固定的架构，这种方式对服务器资源要求比较高，并发局限于应用中，JDK1.4以前的唯一选择，但程序直观简单易理解。</li>\n<li>NIO方式适用于连接数目多且连接比较短（轻操作）的架构，比如聊天服务器，并发局限于应用中，编程比较复杂，JDK1.4开始支持。</li>\n<li>AIO方式适用于连接数目多且连接比较长（重操作）的架构，比如相册服务器，充分调用OS参与并发操作，编程比较复杂，JDK7开始支持。</li>\n</ul>\n<hr />\n<h1>IO多路复用</h1>\n<div>\n<div>\n<p>IO多路复用是要和NIO一起使用的。尽管在操作系统级别，NIO和IO多路复用是两个相对独立的事情。NIO仅仅是指IO API总是能立刻返回，不会被Blocking；而IO多路复用仅仅是操作系统提供的一种便利的通知机制。操作系统并不会强制这俩必须得一起用&mdash;&mdash;你可以用NIO，但不用IO多路复用，就像上一节中的代码；也可以只用IO多路复用 + BIO，这时效果还是当前线程被卡住。但是，<strong>IO多路复用和NIO是要配合一起使用才有实际意义</strong>。</p>\n<p>对IO多路复用，还存在一些常见的误解，比如：</p>\n<ul>\n<li>\n<p><strong>❌IO多路复用是指多个数据流共享同一个Socket</strong>。其实IO多路复用说的是多个Socket，只不过操作系统是一起监听他们的事件而已。</p>\n</li>\n<li>\n<p><strong>❌IO多路复用是NIO，所以总是不Block的</strong>。其实IO多路复用的关键API调用(<code>select</code>，<code>poll</code>，<code>epoll_wait</code>）总是Block的，正如下文的例子所讲。</p>\n</li>\n<li>\n<p>❌<strong>IO多路复用和NIO一起减少了IO</strong>。实际上，IO本身（网络数据的收发）无论用不用IO多路复用和NIO，都没有变化。请求的数据该是多少还是多少；网络上该传输多少数据还是多少数据。IO多路复用和NIO一起仅仅是解决了调度的问题，避免CPU在这个过程中的浪费，使系统的瓶颈更容易触达到网络带宽，而非CPU或者内存。要提高IO吞吐，还是提高硬件的容量（例如，用支持更大带宽的网线、网卡和交换机）和依靠并发传输（例如HDFS的数据多副本并发传输）。</p>\n</li>\n</ul>\n<p>操作系统级别提供了一些接口来支持IO多路复用，最老掉牙的是<code>select</code>和<code>poll</code>。</p>\n</div>\n<div>\n<div>\n<p>但是目前来看，高性能的web服务器都不会使用<code>select</code>和<code>poll</code>。他们俩存在的意义仅仅是&ldquo;兼容性&rdquo;，因为很多操作系统都实现了这两个系统调用。</p>\n<p>如果是追求性能的话，在BSD/macOS上提供了kqueue api；在Salorias中提供了/dev/poll（可惜该操作系统已经凉凉)；而在Linux上提供了epoll api。它们的出现彻底解决了<code>select</code>和<code>poll</code>的问题。Java NIO，nginx等在对应的平台的上都是使用这些api实现。</p>\n<p>因为大部分情况下我会用Linux做服务器，所以下文以Linux epoll为例子来解释多路复用是怎么工作的。</p>\n<div>\n<div>\n<h1>用epoll实现的IO多路复用</h1>\n<p>epoll是Linux下的IO多路复用的实现。并且Linux也是目前最广泛被作为服务器的操作系统。细致的了解epoll对整个IO多路复用的工作原理非常有帮助。</p>\n</div>\n<div>\n<div>\n<p>为什么epoll的性能比<code>select</code>和<code>poll</code>要强呢？ <code>select</code>和<code>poll</code>每次都需要把完成的fd列表传入到内核，迫使内核每次必须从头扫描到尾。而epoll完全是反过来的。epoll在内核的数据被建立好了之后，每次某个被监听的fd一旦有事件发生，内核就直接标记之。<code>epoll_wait</code>调用时，会尝试直接读取到当时已经标记好的fd列表，如果没有就会进入等待状态。</p>\n<p>同时，<code>epoll_wait</code>直接只返回了被触发的fd列表，这样上层应用写起来也轻松愉快，再也不用从大量注册的fd中筛选出有事件的fd了。</p>\n<p>简单说就是<code>select</code>和<code>poll</code>的代价是<strong>\"O(所有注册事件fd的数量)\"</strong>，而epoll的代价是<strong>\"O(发生事件fd的数量)\"</strong>。于是，高性能网络服务器的场景特别适合用epoll来实现&mdash;&mdash;因为大多数网络服务器都有这样的模式：同时要监听大量（几千，几万，几十万甚至更多）的网络连接，但是短时间内发生的事件非常少。</p>\n<p>但是，假设发生事件的fd的数量接近所有注册事件fd的数量，那么epoll的优势就没有了，其性能表现会和<code>poll</code>和<code>select</code>差不多。</p>\n<p>epoll除了性能优势，还有一个优点&mdash;&mdash;同时支持水平触发(Level Trigger)和边沿触发(Edge Trigger)。</p>\n</div>\n</div>\n因为博主水平有限只是对网络上文章做了部分摘取，更多详细内容可以原文进行查看。</div>\n<p><a href=\"https://www.jianshu.com/p/ef418ccf2f7d\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>\n</div>\n</div>\n</div>', '在网络编程中，接触到最多的就是利用Socket进行网络通信开发。在Java中主要是以下三种实现方式BIO、NIO、AIO。关于这三个概念的辨析以前一直都是好像懂，但是表达的不是很清楚，下面做个总结完全辨析清楚。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 6, '2019-12-09 21:01:11', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (56, 'Proxy.newProxyInstance', '<p>在java的动态代理机制中，有两个重要的类或接口，一个是 InvocationHandler(Interface)、另一个则是 Proxy(Class)，这一个类和接口是实现我们动态代理所必须用到的。首先我们先来看看java的API帮助文档是怎么样对这两个类进行描述的：</p>\n<p>InvocationHandler：</p>\n<pre class=\"language-java\"><code>InvocationHandler is the interface implemented by the invocation handler of a proxy instance. \n \nEach proxy instance has an associated invocation handler. When a method is invoked on a proxy instance, the method invocation is encoded and dispatched to the invoke method of its invocation handler.</code></pre>\n<p>每一个动态代理类都必须要实现InvocationHandler这个接口，并且每个代理类的实例都关联到了一个handler，当我们通过代理对象调用一个方法的时候，这个方法的调用就会被转发为由InvocationHandler这个接口的 invoke 方法来进行调用。我们来看看InvocationHandler这个接口的唯一一个方法&nbsp;invoke&nbsp;方法：</p>\n<pre class=\"language-java\"><code>Object invoke(Object proxy, Method method, Object[] args) throws Throwable</code></pre>\n<p>我们看到这个方法一共接受三个参数，那么这三个参数分别代表什么呢？</p>\n<pre class=\"language-java\"><code>Object invoke(Object proxy, Method method, Object[] args) throws Throwable\nproxy:　 - 指代我们所代理的那个真实对象\nmethod:　- 指代的是我们所要调用真实对象的某个方法的Method对象\nargs:　　- 指代的是调用真实对象某个方法时接受的参数</code></pre>\n<p>如果不是很明白，等下通过一个实例会对这几个参数进行更深的讲解。</p>\n<p>接下来我们来看看 Proxy 这个类：</p>\n<pre class=\"language-java\"><code>Proxy provides static methods for creating dynamic proxy classes and instances, and it is also the superclass of all dynamic proxy classes created by those methods. </code></pre>\n<p>Proxy这个类的作用就是用来动态创建一个代理对象的类，它提供了许多的方法，但是我们用的最多的就是&nbsp;newProxyInstance&nbsp;这个方法：</p>\n<pre class=\"language-java\"><code>public static Object newProxyInstance(ClassLoader loader, Class&lt;?&gt;[] interfaces,  InvocationHandler h)  throws IllegalArgumentException</code></pre>\n<p>这个方法的作用就是得到一个动态的代理对象，其接收三个参数，我们来看看这三个参数所代表的含义：</p>\n<pre class=\"language-java\"><code>public static Object newProxyInstance(ClassLoader loader, Class&lt;?&gt;[] interfaces, InvocationHandler h) throws IllegalArgumentException\nloader:　　    一个ClassLoader对象，定义了由哪个ClassLoader对象来对生成的代理对象进行加载\ninterfaces:　　一个Interface对象的数组，表示的是我将要给我需要代理的对象提供一组什么接口，如果我提供了一组接口给它，那么这个代理对象就宣称实现了该接口(多态)，这样我就能调用这组接口中的方法了\nh:　　         一个InvocationHandler对象，表示的是当我这个动态代理对象在调用方法的时候，会关联到哪一个InvocationHandler对象上</code></pre>\n<hr />\n<p>好了，在介绍完这两个接口(类)以后，我们来通过一个实例来看看我们的动态代理模式是什么样的：</p>\n<p>首先我们定义了一个Subject类型的接口，为其声明了两个方法：</p>\n<pre class=\"language-java\"><code>public interface Subject\n{\n    public void rent();\n    \n    public void hello(String str);\n}</code></pre>\n<p>接着，定义了一个类来实现这个接口，这个类就是我们的真实对象，RealSubject类：</p>\n<pre class=\"language-java\"><code>public class RealSubject implements Subject\n{\n    @Override\n    public void rent()\n    {\n        System.out.println(\"I want to rent my house\");\n    }\n    \n    @Override\n    public void hello(String str)\n    {\n        System.out.println(\"hello: \" + str);\n    }\n}</code></pre>\n<p>下一步，我们就要定义一个动态代理类了，前面说个，每一个动态代理类都必须要实现 InvocationHandler 这个接口，因此我们这个动态代理类也不例外：</p>\n<pre class=\"language-java\"><code>public class DynamicProxy implements InvocationHandler\n{\n    // 这个就是我们要代理的真实对象\n    private Object subject;\n    \n    // 构造方法，给我们要代理的真实对象赋初值\n    public DynamicProxy(Object subject)\n    {\n        this.subject = subject;\n    }\n    \n    @Override\n    public Object invoke(Object object, Method method, Object[] args)\n            throws Throwable\n    {\n        // 在代理真实对象前我们可以添加一些自己的操作\n        System.out.println(\"before rent house\");\n        \n        System.out.println(\"Method:\" + method);\n        \n        // 当代理对象调用真实对象的方法时，其会自动的跳转到代理对象关联的handler对象的invoke方法来进行调用\n        method.invoke(subject, args);\n        \n        // 在代理真实对象后我们也可以添加一些自己的操作\n        System.out.println(\"after rent house\");\n        \n        return null;\n    }\n}</code></pre>\n<p>最后，来看看我们的Client类：</p>\n<pre class=\"language-java\"><code>public class Client\n{\n    public static void main(String[] args)\n    {\n        // 我们要代理的真实对象\n        Subject realSubject = new RealSubject();\n \n        // 我们要代理哪个真实对象，就将该对象传进去，最后是通过该真实对象来调用其方法的\n        InvocationHandler handler = new DynamicProxy(realSubject);\n \n        /*\n         * 通过Proxy的newProxyInstance方法来创建我们的代理对象，我们来看看其三个参数\n         * 第一个参数 handler.getClass().getClassLoader() ，我们这里使用handler这个类的ClassLoader对象来加载我们的代理对象\n         * 第二个参数realSubject.getClass().getInterfaces()，我们这里为代理对象提供的接口是真实对象所实行的接口，表示我要代理的是该真实对象，这样我就能调用这组接口中的方法了\n         * 第三个参数handler， 我们这里将这个代理对象关联到了上方的 InvocationHandler 这个对象上\n         */\n        Subject subject = (Subject)Proxy.newProxyInstance(handler.getClass().getClassLoader(), realSubject\n                .getClass().getInterfaces(), handler);\n        \n        System.out.println(subject.getClass().getName());\n        subject.rent();\n        subject.hello(\"world\");\n    }\n}</code></pre>\n<p>我们先来看看控制台的输出：</p>\n<pre class=\"language-java\"><code>$Proxy0\n \nbefore rent house\nMethod:public abstract void com.xiaoluo.dynamicproxy.Subject.rent()\nI want to rent my house\nafter rent house\n \nbefore rent house\nMethod:public abstract void com.xiaoluo.dynamicproxy.Subject.hello(java.lang.String)\nhello: world\nafter rent house</code></pre>\n<p><a href=\"https://blog.csdn.net/Dream_Weave/article/details/84183247\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>', '在学习Spring的时候，我们知道Spring主要有两大思想，一个是IoC，另一个就是AOP，对于IoC，而对于Spring的核心AOP来说，我们不但要知道怎么通过AOP来满足的我们的功能，我们更需要学习的是其底层是怎么样的一个原理，而AOP的原理就是java的动态代理机制。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 6, '2019-12-10 18:03:23', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (57, 'ReadWriteLock', '<h2>基本讲解与使用</h2>\n<p>① ReadWriteLock同Lock一样也是一个接口，提供了readLock和writeLock两种锁的操作机制，一个是只读的锁，一个是写锁。</p>\n<p>读锁可以在没有写锁的时候被多个线程同时持有，写锁是独占的(排他的)。 每次只能有一个写线程，但是可以有多个线程并发地读数据。</p>\n<p>所有读写锁的实现必须确保写操作对读操作的内存影响。换句话说，一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。</p>\n<p>理论上，读写锁比互斥锁允许对于共享数据更大程度的并发。与互斥锁相比，读写锁是否能够提高性能取决于读写数据的频率、读取和写入操作的持续时间、以及读线程和写线程之间的竞争。</p>\n<p>② 使用场景</p>\n<p>假设你的程序中涉及到对一些共享资源的读和写操作，且写操作没有读操作那么频繁。</p>\n<p>例如，最初填充有数据，然后很少修改的集合，同时频繁搜索（例如某种目录）是使用读写锁的理想候选项。</p>\n<p>在没有写操作的时候，两个线程同时读一个资源没有任何问题，所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源，就不应该再有其它线程对该资源进行读或写。这就需要一个读/写锁来解决这个问题。</p>\n<p>③ 互斥原则：</p>\n<p>读-读能共存，<br />读-写不能共存，<br />写-写不能共存。</p>\n<pre class=\"language-java\"><code>import java.util.concurrent.locks.ReadWriteLock;\nimport java.util.concurrent.locks.ReentrantReadWriteLock;\n\npublic class ReadWriteLockDemo {\n    private ReadWriteLock readWriteLock= new ReentrantReadWriteLock();\n    private int number=0;\n\n    public void get(){\n        readWriteLock.readLock().lock();\n        try{\n            System.out.println(Thread.currentThread().getName()+\"线程读取中。。\");\n            System.out.println(\"number:\"+number);\n        }finally {\n            readWriteLock.readLock().unlock();\n        }\n    }\n\n    public void set(Integer num){\n        readWriteLock.writeLock().lock();\n        try{\n            System.out.println(Thread.currentThread().getName()+\"线程写入中...........................................\");\n            Thread.sleep(1000);\n            this.number=num;\n            System.out.println(\"number:\"+number);\n            System.out.println(\"写入成功！\");\n            System.out.println();\n        } catch (InterruptedException e) {\n            e.printStackTrace();\n        } finally {\n            readWriteLock.writeLock().unlock();\n        }\n    }\n    public static void main(String[] args) throws InterruptedException {\n        //创建30个读线程，10个写线程\n        ReadWriteLockDemo readWriteLockDemo=new ReadWriteLockDemo();\n        for (int i = 0; i &lt; 30; i++) {\n            final int L=i;\n            new Thread(()-&gt;{\n                readWriteLockDemo.get();\n            },L+\"线程\").start();\n        }\n\n        for (int i = 40; i &lt; 50; i++) {\n            final int L=i;\n            new Thread(()-&gt;{\n                readWriteLockDemo.set(L);\n            },L+\"线程\").start();\n        }\n\n\n    }\n\n}\n</code></pre>\n<pre class=\"language-java\"><code>\n0线程线程读取中。。\nnumber:0\n1线程线程读取中。。\nnumber:0\n2线程线程读取中。。\nnumber:0\n4线程线程读取中。。\nnumber:0\n3线程线程读取中。。\nnumber:0\n6线程线程读取中。。\nnumber:0\n5线程线程读取中。。\n7线程线程读取中。。\nnumber:0\n8线程线程读取中。。\nnumber:0\n10线程线程读取中。。\nnumber:0\nnumber:0\n9线程线程读取中。。\nnumber:0\n11线程线程读取中。。\nnumber:0\n12线程线程读取中。。\nnumber:0\n13线程线程读取中。。\nnumber:0\n16线程线程读取中。。\nnumber:0\n15线程线程读取中。。\nnumber:0\n14线程线程读取中。。\nnumber:0\n17线程线程读取中。。\nnumber:0\n19线程线程读取中。。\nnumber:0\n18线程线程读取中。。\nnumber:0\n22线程线程读取中。。\nnumber:0\n20线程线程读取中。。\nnumber:0\n24线程线程读取中。。\nnumber:0\n26线程线程读取中。。\nnumber:0\n28线程线程读取中。。\nnumber:0\n40线程线程写入中...........................................\nnumber:40\n写入成功！\n\n21线程线程读取中。。\nnumber:40\n42线程线程写入中...........................................\nnumber:42\n写入成功！\n\n44线程线程写入中...........................................\nnumber:44\n写入成功！\n\n29线程线程读取中。。\nnumber:44\n46线程线程写入中...........................................\nnumber:46\n写入成功！\n\n43线程线程写入中...........................................\nnumber:43\n写入成功！\n\n25线程线程读取中。。\n23线程线程读取中。。\nnumber:43\nnumber:43\n47线程线程写入中...........................................\nnumber:47\n写入成功！\n\n27线程线程读取中。。\nnumber:47\n49线程线程写入中...........................................\nnumber:49\n写入成功！\n\n45线程线程写入中...........................................\nnumber:45\n写入成功！\n\n41线程线程写入中...........................................\nnumber:41\n写入成功！\n\n48线程线程写入中...........................................\nnumber:48\n写入成功！\n\n\n</code></pre>', 'ReadWriteLock同Lock一样也是一个接口，提供了readLock和writeLock两种锁的操作机制，一个是只读的锁，一个是写锁。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 1, '2019-12-12 10:12:01', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (58, 'SQL JOINS', '<p>SQL语句查询的机器执行顺序：</p>\n<ol>\n<li>FROM</li>\n<li>ON</li>\n<li>JOIN</li>\n<li>WHERE</li>\n<li>GROUP BY</li>\n<li>CUBE|ROLLUP</li>\n<li>HAVING</li>\n<li>SELECT</li>\n<li>DISTINCT</li>\n<li>ORDER BY</li>\n<li>LIMIT</li>\n</ol>\n<hr />\n<p><img src=\"https://timgsa.baidu.com/timg?image&amp;quality=80&amp;size=b9999_10000&amp;sec=1576366392638&amp;di=21e7e36ce92143fc5524898e768b92ab&amp;imgtype=0&amp;src=http%3A%2F%2Fattach.dataguru.cn%2Fattachments%2Fforum%2F201304%2F09%2F210610grkvqb0i0rb3tobc.jpg\" alt=\"\" width=\"966\" height=\"760\" /></p>\n<p>Mysql不支持 FULL OUTER JOIN</p>\n<p>所以最后两种需使用 UNION</p>\n<pre class=\"language-markup\"><code>UNION 操作符用于合并两个或多个 SELECT 语句的结果集。\n\n请注意，UNION 内部的 SELECT 语句必须拥有相同数量的列。列也必须拥有相似的数据类型。同时，每条 SELECT 语句中的列的顺序必须相同。\n注释：默认地，UNION 操作符选取不同的值。如果允许重复的值，请使用 UNION ALL。</code></pre>\n<pre class=\"language-markup\"><code>select * from t1 left join t2 on t1.prid=t2.id\nunion\nselect * from t1 right join t2 on t1.prid=t2.id</code></pre>\n<pre class=\"language-markup\"><code>select * from t1 left join t2 on t1.prid=t2.id where t2.id is null\nunion\nselect * from t1 right join t2 on t1.prid=t2.id where t1.prid is null</code></pre>', 'SQL机器执行顺序，以及7种JOIN。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 4, '2019-12-15 15:27:34', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (59, 'Redis持久化（RDB和AOF）', '<h3 id=\"item-1-2\">持久化的实现方式</h3>\n<h4><strong>快照方式持久化</strong></h4>\n<p>快照方式持久化就是在某时刻把所有数据进行完整备份。</p>\n<p>例：Mysql的Dump方式、<span style=\"color: #ff0000;\">Redis的RDB</span>方式。</p>\n<h4><strong>写日志方式持久化</strong></h4>\n<p>写日志方式持久化就是把用户执行的所有写指令（增删改）备份到文件中，还原数据时只需要把备份的所有指令重新执行一遍即可。</p>\n<p>例：Mysql的Binlog、<span style=\"color: #ff0000;\">Redis的AOF</span>、Hbase的HLog。</p>\n<hr />\n<h2 id=\"item-2\">RDB</h2>\n<p><img src=\"https://image-static.segmentfault.com/200/180/2001805000-5b73cfc79b2d5_articlex\" alt=\"\" /></p>\n<p>RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储。<br />在默认情况下， Redis 将数据库快照保存在名字为 dump.rdb的二进制文件中。<br />在 Redis 运行时， RDB 程序将当前内存中的数据库快照保存到磁盘文件中， 在 Redis 重启动时， RDB 程序可以通过载入 RDB 文件来还原数据库的状态。</p>\n<h3><strong>工作方式</strong></h3>\n<p>当 Redis 需要保存 dump.rdb 文件时， 服务器执行以下操作:</p>\n<ol>\n<li>Redis 调用forks。同时拥有父进程和子进程。</li>\n<li>子进程将数据集写入到一个临时 RDB 文件中。</li>\n<li>当子进程完成对新 RDB 文件的写入时，Redis 用新 RDB 文件替换原来的 RDB 文件，并删除旧的 RDB 文件。</li>\n</ol>\n<p>这种工作方式使得 Redis 可以从写时复制（copy-on-write）机制中获益。</p>\n<h3 id=\"item-2-4\">RDB的三种主要触发机制</h3>\n<h4><strong>save命令（同步数据到磁盘上）</strong></h4>\n<p><code>save</code>&nbsp;命令执行一个同步操作，以RDB文件的方式保存所有数据的快照。</p>\n<pre class=\"hljs css\"><code>127<span class=\"hljs-selector-class\">.0</span><span class=\"hljs-selector-class\">.0</span><span class=\"hljs-selector-class\">.1</span><span class=\"hljs-selector-pseudo\">:6379</span>&gt; <span class=\"hljs-selector-tag\">save</span>\n<span class=\"hljs-selector-tag\">OK</span></code><br /><br /><img src=\"https://image-static.segmentfault.com/381/183/381183997-5b73cfc78f7e3_articlex\" alt=\"\" /><br /><br />由于&nbsp;<span style=\"color: #ff0000;\"><code>save</code>&nbsp;命令是同步命令，会占用Redis的主进程</span>。若Redis数据非常多时，<code>save</code>命令执行速度会非常慢，阻塞所有客户端的请求。<br />因此很少在生产环境直接使用SAVE 命令，可以使用BGSAVE 命令代替。如果在BGSAVE命令的保存数据的子进程发生错误的时，用 SAVE命令保存最新的数据是最后的手段。<br /><img src=\"https://image-static.segmentfault.com/124/101/1241012198-5b73cfc7710b8_articlex\" alt=\"\" /></pre>\n<h4><strong>bgsave命令（异步保存数据到磁盘上）</strong></h4>\n<p><span style=\"color: #ff0000;\"><code>bgsave</code>&nbsp;命令执行一个异步操作，以RDB文件的方式保存所有数据的快照。</span></p>\n<pre class=\"hljs css\"><code>127<span class=\"hljs-selector-class\">.0</span><span class=\"hljs-selector-class\">.0</span><span class=\"hljs-selector-class\">.1</span><span class=\"hljs-selector-pseudo\">:6379</span>&gt; <span class=\"hljs-selector-tag\">bgsave</span>\n<span class=\"hljs-selector-tag\">Background</span> <span class=\"hljs-selector-tag\">saving</span> <span class=\"hljs-selector-tag\">started</span>\n</code></pre>\n<p>Redis使用Linux系统的<code>fock()</code>生成一个子进程来将DB数据保存到磁盘，主进程继续提供服务以供客户端调用。<br />如果操作成功，可以通过客户端命令LASTSAVE来检查操作结果。</p>\n<p><img src=\"https://image-static.segmentfault.com/602/904/602904611-5b73cfc740cfe_articlex\" alt=\"\" /></p>\n<h4><strong><code>save</code>&nbsp;与&nbsp;<code>bgsave</code>&nbsp;对比</strong></h4>\n<table>\n<thead>\n<tr>\n<th>命令</th>\n<th>save</th>\n<th>bgsave</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>IO类型</td>\n<td>同步</td>\n<td>异步</td>\n</tr>\n<tr>\n<td>阻塞？</td>\n<td>是</td>\n<td>是（阻塞发生在fock()，通常非常快）</td>\n</tr>\n<tr>\n<td>复杂度</td>\n<td>O(n)</td>\n<td>O(n)</td>\n</tr>\n<tr>\n<td>优点</td>\n<td>不会消耗额外的内存</td>\n<td>不阻塞客户端命令</td>\n</tr>\n<tr>\n<td>缺点</td>\n<td>阻塞客户端命令</td>\n<td>需要fock子进程，消耗内存</td>\n</tr>\n</tbody>\n</table>\n<h3 id=\"item-2-5\">RDB相关配置</h3>\n<pre class=\"language-markup\"><code># RDB自动持久化规则\n# 当 900 秒内有至少有 1 个键被改动时，自动进行数据集保存操作\nsave 900 1\n# 当 300 秒内有至少有 10 个键被改动时，自动进行数据集保存操作\nsave 300 10\n# 当 60 秒内有至少有 10000 个键被改动时，自动进行数据集保存操作\nsave 60 10000\n\n# RDB持久化文件名\ndbfilename dump-&lt;port&gt;.rdb\n\n# 数据持久化文件存储目录\ndir /var/lib/redis\n\n# bgsave发生错误时是否停止写入，通常为yes\nstop-writes-on-bgsave-error yes\n\n# rdb文件是否使用压缩格式\nrdbcompression yes\n\n# 是否对rdb文件进行校验和检验，通常为yes\nrdbchecksum yes</code></pre>\n<h3 id=\"item-2-6\">RDB的优点</h3>\n<ol>\n<li>RDB是一个非常紧凑的文件，它保存了某个时间点得数据集，非常适用于数据集的备份，比如你可以在每个小时报保存一下过去24小时内的数据，同时每天保存过去30天的数据，这样即使出了问题你也可以根据需求恢复到不同版本的数据集。</li>\n<li>RDB是一个紧凑的单一文件，很方便传送到另一个远端数据中心或者亚马逊的S3（可能加密），非常适用于灾难恢复。</li>\n<li>RDB在保存RDB文件时父进程唯一需要做的就是fork出一个子进程，接下来的工作全部由子进程来做，父进程不需要再做其他IO操作，所以RDB持久化方式可以最大化redis的性能。</li>\n<li>与AOF相比，在恢复大的数据集的时候，RDB方式会更快一些。</li>\n</ol>\n<h3 id=\"item-2-7\">RDB的缺点</h3>\n<ol>\n<li>耗时、耗性能。RDB 需要经常fork子进程来保存数据集到硬盘上，当数据集比较大的时候，fork的过程是非常耗时的，可能会导致Redis在一些毫秒级内不能响应客户端的请求。如果数据集巨大并且CPU性能不是很好的情况下，这种情况会持续1秒，AOF也需要fork，但是你可以调节重写日志文件的频率来提高数据集的耐久度。</li>\n<li>不可控、丢失数据。如果你希望在redis意外停止工作（例如电源中断）的情况下丢失的数据最少的话，那么RDB不适合你。虽然你可以配置不同的save时间点(例如每隔5分钟并且对数据集有100个写的操作)，是Redis要完整的保存整个数据集是一个比较繁重的工作，你通常会每隔5分钟或者更久做一次完整的保存，万一在Redis意外宕机，你可能会丢失几分钟的数据。</li>\n</ol>\n<hr />\n<h2 id=\"item-3\">AOF</h2>\n<p>打开AOF后， <span style=\"color: #ff0000;\">每当 Redis 执行一个改变数据集的命令时（比如 SET）， 这个命令就会被追加到 AOF 文件的末尾。</span>这样的话， 当 Redis 重新启时， 程序就可以通过重新执行 AOF 文件中的命令来达到重建数据集的目的。</p>\n<h4><strong>AOF运行原理 - 创建</strong></h4>\n<p><strong><img src=\"https://image-static.segmentfault.com/351/968/351968413-5b73cfc7205e7_articlex\" alt=\"\" /></strong></p>\n<h4><strong>AOF运行原理 - 恢复</strong></h4>\n<p><strong><img src=\"https://image-static.segmentfault.com/115/755/1157551996-5b73cfc79cdb5_articlex\" alt=\"\" /></strong></p>\n<h3 id=\"item-3-9\">AOF持久化的三种策略</h3>\n<p>你可以通过配置文件配置 Redis 多久才将数据 fsync 到磁盘一次。</p>\n<h4><strong>always</strong></h4>\n<p>每次有新命令追加到 AOF 文件时就执行一次 fsync ：非常慢，也非常安全。</p>\n<p><img src=\"https://image-static.segmentfault.com/125/754/1257547519-5b73cfc79c755_articlex\" alt=\"\" /></p>\n<h4><strong>everysec</strong></h4>\n<p>每秒 fsync 一次：足够快（和使用 RDB 持久化差不多），并且在故障时只会丢失 1 秒钟的数据。<br />推荐（并且也是默认）的措施为每秒 fsync 一次， 这种 fsync 策略可以兼顾速度和安全性。</p>\n<p><img src=\"https://image-static.segmentfault.com/948/114/948114448-5b73cfc6d59bc_articlex\" alt=\"\" /></p>\n<h4><strong>no</strong></h4>\n<p>从不 fsync ：将数据交给操作系统来处理，由操作系统来决定什么时候同步数据。更快，也更不安全的选择。</p>\n<p><img src=\"https://image-static.segmentfault.com/122/826/1228264883-5b73cfc6d23f9_articlex\" alt=\"\" /></p>\n<h4><strong>always、everysec、no对比</strong></h4>\n<table>\n<thead>\n<tr>\n<th>命令</th>\n<th>优点</th>\n<th>缺点</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>always</td>\n<td>不丢失数据</td>\n<td>IO开销大，一般SATA磁盘只有几百TPS</td>\n</tr>\n<tr>\n<td>everysec</td>\n<td>每秒进行与fsync，最多丢失1秒数据</td>\n<td>可能丢失1秒数据</td>\n</tr>\n<tr>\n<td>no</td>\n<td>不用管</td>\n<td>不可控</td>\n</tr>\n</tbody>\n</table>\n<p>推荐（并且也是默认）的措施为每秒 fsync 一次， 这种 fsync 策略可以兼顾速度和安全性。</p>\n<h3 id=\"item-3-10\">AOF重写</h3>\n<p>因为 AOF 的运作方式是不断地将命令追加到文件的末尾， 所以随着写入命令的不断增加， AOF 文件的体积也会变得越来越大。举个例子， 如果你对一个计数器调用了 100 次 INCR ， 那么仅仅是为了保存这个计数器的当前值， AOF 文件就需要使用 100 条记录（entry）。然而在实际上， 只使用一条 SET 命令已经足以保存计数器的当前值了， 其余 99 条记录实际上都是多余的。<br />为了处理这种情况， Redis 支持一种有趣的特性： 可以在不打断服务客户端的情况下， 对 AOF 文件进行重建（rebuild）。执行 bgrewriteaof 命令， Redis 将生成一个新的 AOF 文件， 这个文件包含重建当前数据集所需的最少命令。<br />Redis 2.2 需要自己手动执行 bgrewriteaof 命令； Redis 2.4 则可以通过配置自动触发 AOF 重写。</p>\n<p><img src=\"https://image-static.segmentfault.com/181/263/1812631962-5b73cfc6d572d_articlex\" alt=\"\" /></p>\n<h4><strong>AOF重写的作用</strong></h4>\n<ul>\n<li>减少磁盘占用量</li>\n<li>加速数据恢复</li>\n</ul>\n<h4><strong>AOF重写的实现方式</strong></h4>\n<ul>\n<li>\n<p><strong>bgrewriteaof 命令</strong></p>\n<p>Redis bgrewriteaof 命令用于异步执行一个 AOF（AppendOnly File）文件重写操作。重写会创建一个当前AOF文件的体积优化版本。<br />即使 bgrewriteaof 执行失败，也不会有任何数据丢失，因为旧的AOF文件在 bgrewriteaof 成功之前不会被修改。<br />AOF 重写由 Redis 自行触发，bgrewriteaof 仅仅用于手动触发重写操作。<br />具体内容:</p>\n<ul>\n<li>如果一个子Redis是通过磁盘快照创建的，AOF重写将会在RDB终止后才开始保存。这种情况下BGREWRITEAOF任然会返回OK状态码。从Redis 2.6起你可以通过INFO命令查看AOF重写执行情况。</li>\n<li>如果只在执行的AOF重写返回一个错误，AOF重写将会在稍后一点的时间重新调用。</li>\n</ul>\n</li>\n</ul>\n<p><img src=\"https://image-static.segmentfault.com/360/676/3606763245-5b73cfc6d009c_articlex\" alt=\"\" /></p>\n<ul>\n<li><strong>AOF重写配置</strong></li>\n</ul>\n<table>\n<thead>\n<tr>\n<th>配置名</th>\n<th>含义</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>auto-aof-rewrite-min-size</td>\n<td>触发AOF文件执行重写的最小尺寸</td>\n</tr>\n<tr>\n<td>auto-aof-rewrite-percentage</td>\n<td>触发AOF文件执行重写的增长率</td>\n</tr>\n</tbody>\n</table>\n<table>\n<thead>\n<tr>\n<th>统计名</th>\n<th>含义</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>aof_current_size</td>\n<td>AOF文件当前尺寸（字节）</td>\n</tr>\n<tr>\n<td>aof_base_size</td>\n<td>AOF文件上次启动和重写时的尺寸（字节）</td>\n</tr>\n</tbody>\n</table>\n<blockquote>\n<p>AOF重写自动触发机制，需要同时满足下面两个条件：</p>\n<ul>\n<li>aof_current_size &gt; auto-aof-rewrite-min-size</li>\n<li>(aof_current_size - aof_base_size) * 100 / aof_base_size &gt; auto-aof-rewrite-percentage</li>\n</ul>\n</blockquote>\n<p>假设 Redis 的配置项为：</p>\n<pre class=\"hljs arduino\"><code><span class=\"hljs-keyword\">auto</span>-aof-rewrite-<span class=\"hljs-built_in\">min</span>-<span class=\"hljs-built_in\">size</span> <span class=\"hljs-number\">64</span>mb\n<span class=\"hljs-keyword\">auto</span>-aof-rewrite-percentage <span class=\"hljs-number\">100</span></code></pre>\n<p>当AOF文件的体积大于64Mb，并且AOF文件的体积比上一次重写之久的体积大了至少一倍（100%）时，Redis将执行 bgrewriteaof 命令进行重写。</p>\n<h3 id=\"item-3-11\">AOF相关配置</h3>\n<pre class=\"language-markup\"><code># 开启AOF持久化方式\nappendonly yes\n\n# AOF持久化文件名\nappendfilename appendonly-&lt;port&gt;.aof\n\n# 每秒把缓冲区的数据同步到磁盘\nappendfsync everysec\n\n# 数据持久化文件存储目录\ndir /var/lib/redis\n\n# 是否在执行重写时不同步数据到AOF文件\n# 这里的 yes，就是执行重写时不同步数据到AOF文件\nno-appendfsync-on-rewrite yes\n\n# 触发AOF文件执行重写的最小尺寸\nauto-aof-rewrite-min-size 64mb\n\n# 触发AOF文件执行重写的增长率\nauto-aof-rewrite-percentage 100</code></pre>\n<h3 id=\"item-3-12\">AOF的优点</h3>\n<ol>\n<li>使用AOF 会让你的Redis更加耐久: 你可以使用不同的fsync策略：无fsync，每秒fsync，每次写的时候fsync。使用默认的每秒fsync策略，Redis的性能依然很好(fsync是由后台线程进行处理的，主线程会尽力处理客户端请求)，一旦出现故障，你最多丢失1秒的数据。</li>\n<li>AOF文件是一个只进行追加的日志文件，所以不需要写入seek，即使由于某些原因(磁盘空间已满，写的过程中宕机等等)未执行完整的写入命令，你也也可使用redis-check-aof工具修复这些问题。</li>\n<li>Redis 可以在 AOF 文件体积变得过大时，自动地在后台对 AOF 进行重写： 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的，因为 Redis 在创建新 AOF 文件的过程中，会继续将命令追加到现有的 AOF 文件里面，即使重写过程中发生停机，现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕，Redis 就会从旧 AOF 文件切换到新 AOF 文件，并开始对新 AOF 文件进行追加操作。</li>\n<li>AOF 文件有序地保存了对数据库执行的所有写入操作， 这些写入操作以 Redis 协议的格式保存， 因此 AOF 文件的内容非常容易被人读懂， 对文件进行分析（parse）也很轻松。 导出（export） AOF 文件也非常简单： 举个例子， 如果你不小心执行了 FLUSHALL 命令， 但只要 AOF 文件未被重写， 那么只要停止服务器， 移除 AOF 文件末尾的 FLUSHALL 命令， 并重启 Redis ， 就可以将数据集恢复到 FLUSHALL 执行之前的状态。</li>\n</ol>\n<h3 id=\"item-3-13\">AOF的缺点</h3>\n<ol>\n<li>对于相同的数据集来说，AOF 文件的体积通常要大于 RDB 文件的体积。</li>\n<li>根据所使用的 fsync 策略，AOF 的速度可能会慢于 RDB 。 在一般情况下， 每秒 fsync 的性能依然非常高， 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快， 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时，RDB 可以提供更有保证的最大延迟时间（latency）。</li>\n</ol>\n<hr />\n<h3 id=\"item-4-14\">RDB 和 AOF 对比</h3>\n<table>\n<thead>\n<tr>\n<th>-</th>\n<th>RDB</th>\n<th>AOF</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>启动优先级</td>\n<td>低</td>\n<td>高</td>\n</tr>\n<tr>\n<td>体积</td>\n<td>小</td>\n<td>大</td>\n</tr>\n<tr>\n<td>恢复速度</td>\n<td>快</td>\n<td>慢</td>\n</tr>\n<tr>\n<td>数据安全性</td>\n<td>丢数据</td>\n<td>根据策略决定</td>\n</tr>\n</tbody>\n</table>\n<h3 id=\"item-4-15\">如何选择使用哪种持久化方式？</h3>\n<p>一般来说， 如果想达到足以媲美 PostgreSQL 的数据安全性， 你应该同时使用两种持久化功能。</p>\n<p>如果你非常关心你的数据， 但仍然可以承受数分钟以内的数据丢失， 那么你可以只使用 RDB 持久化。</p>\n<p>有很多用户都只使用 AOF 持久化， 但并不推荐这种方式： 因为定时生成 RDB 快照（snapshot）非常便于进行数据库备份， 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快。</p>\n<p><a href=\"https://segmentfault.com/a/1190000016021217\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>', '持久化Redis所有数据保持在内存中，对数据的更新将异步地保存到磁盘上。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 4, '2019-12-17 18:20:06', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (60, '178. 分数排名', '<p>编写一个 SQL 查询来实现分数排名。如果两个分数相同，则两个分数排名（Rank）相同。请注意，平分后的下一个名次应该是下一个连续的整数值。换句话说，名次之间不应该有&ldquo;间隔&rdquo;。</p>\n<p><img class=\"wscnph\" src=\"\" /></p>\n<hr />\n<p>【解题思路】</p>\n<p>1.涉及到排名问题，可以使用窗口函数</p>\n<p>2.专用窗口函数rank, dense_rank, row_number有什么区别呢？</p>\n<p>它们的区别我举个例子，你们一下就能看懂：</p>\n<pre class=\"language-markup\"><code>select *,\n   rank() over (order by 成绩 desc) as ranking,\n   dense_rank() over (order by 成绩 desc) as dese_rank,\n   row_number() over (order by 成绩 desc) as row_num\nfrom 班级\n</code></pre>\n<p><img src=\"https://pic.leetcode-cn.com/555db2ac6d57cc9c591c6475de79262f7ba4ecd43142ff0750e09d4d18fdffa6-1.png\" alt=\"\" width=\"635\" height=\"476\" /></p>\n<p>从上面的结果可以看出：<br />1）rank函数：这个例子中是5位，5位，5位，8位，也就是如果有并列名次的行，会占用下一名次的位置。比如正常排名是1，2，3，4，但是现在前3名是并列的名次，结果是：1，1，1，4。</p>\n<p>2）dense_rank函数：这个例子中是5位，5位，5位，6位，也就是如果有并列名次的行，不占用下一名次的位置。比如正常排名是1，2，3，4，但是现在前3名是并列的名次，结果是：1，1，1，2。</p>\n<p>3）row_number函数：这个例子中是5位，6位，7位，8位，也就是不考虑并列名次的情况。比如前3名是并列的名次，排名是正常的1，2，3，4。</p>\n<p>这三个函数的区别如下：</p>\n<p><img src=\"https://pic.leetcode-cn.com/729cc8ee48f55e4c4c448d764e6c0c1e1de50a7cb1674fd557abff50519651a8-1.png\" alt=\"\" width=\"636\" height=\"476\" /></p>\n<p>根据题目要求的排名规则，这里我们使用dense_rank函数。</p>\n<hr />\n<p><a href=\"https://leetcode-cn.com/problems/rank-scores/solution/tu-jie-sqlmian-shi-ti-jing-dian-pai-ming-wen-ti-by/\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>', '编写一个 SQL 查询来实现分数排名。如果两个分数相同，则两个分数排名（Rank）相同。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 2, '2019-12-20 17:03:18', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (61, 'ActiveMq入门', '<h2>JMS&nbsp;</h2>\n<p>Java 消息中间件的服务接口规范，activemq 之上是 mq ， 而 mq 之上是JMS 定义的消息规范 。 activemq 是mq 技术的一种理论实现（与之相类似的实现还有 Kafka RabbitMQ RockitMQ ），而 JMS 是更上一级的规范。</p>\n<p><img src=\"https://note.youdao.com/yws/public/resource/39fb9a2ff543d021d619885806a15f05/xmlnote/ACE9B20C3FA74BB581858A7C58FF8630/12122\" alt=\"\" width=\"972\" height=\"543\" /></p>\n<div>在点对点的消息传递时，目的地称为 队列 queue</div>\n<div>在发布订阅消息传递中，目的地称为 主题 topic</div>\n<hr />\n<p>ActiveMQ的安装与启动</p>\n<p>（1）官网下载对应服务器版本</p>\n<p>（2）解压后进入apache-activemq-5.15.9/bin目录</p>\n<p>（3）执行./activemq start启动ActiveMQ</p>\n<p>（4）浏览器输入ActiveMQ启动的服务器ip:8161便可进入web界面，点击Manage ActiveMQ broker可以查看消息推送的状态，默认账号密码为admin,admin</p>\n<hr />\n<h2>测试 queue Demo</h2>\n<ul>\n<li>生产者，生产5条消息</li>\n</ul>\n<pre class=\"language-markup\"><code>   &lt;!--Maven依赖--&gt;\n    &lt;dependency&gt;\n        &lt;groupId&gt;org.apache.activemq&lt;/groupId&gt;\n        &lt;artifactId&gt;activemq-all&lt;/artifactId&gt;\n        &lt;version&gt;5.9.0&lt;/version&gt;\n    &lt;/dependency&gt;</code></pre>\n<pre class=\"language-java\"><code>import org.apache.activemq.ActiveMQConnectionFactory;\n\nimport javax.jms.*;\n\npublic class MyProducer {\n    private static final String ACTIVEMQ_URL = \"tcp://localhost:61616\";\n    private static final String Que_Name=\"queue01\";\n    public static void main(String[] args) throws JMSException {\n        //1 create factory\n        ActiveMQConnectionFactory activeMQConnectionFactory=new ActiveMQConnectionFactory(ACTIVEMQ_URL);\n        //2 create connection\n        Connection connection = activeMQConnectionFactory.createConnection();\n        //3 create session\n        connection.start();\n        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);\n        //4 queue or topic\n        Queue queue = session.createQueue(Que_Name);\n        //5 create producer\n        MessageProducer producer = session.createProducer(queue);\n        //6 create message\n        for (int i = 0; i &lt; 5; i++) {\n            //创建文本消息\n            TextMessage message = session.createTextMessage(\"第\" + i + \"个文本消息\");\n            //发送消息\n            producer.send(message);\n            System.out.println(message.getText());\n\n        }\n        producer.close();\n        session.close();\n        connection.close();\n\n\n    }\n}\n</code></pre>\n<p>测试查看web后台显示，有5条消息在队列中等待消费</p>\n<p><img class=\"wscnph\" src=\"\" /></p>\n<ul>\n<li>消费者</li>\n</ul>\n<pre class=\"language-java\"><code>import org.apache.activemq.ActiveMQConnectionFactory;\n\nimport javax.jms.*;\n\npublic class MyConsumer {\n    private static final String ACTIVEMQ_URL = \"tcp://localhost:61616\";\n    private static final String Que_Name=\"queue01\";\n\n    public static void main(String[] args) throws JMSException {\n        // 创建连接工厂\n        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);\n        // 创建连接\n        Connection connection = activeMQConnectionFactory.createConnection();\n        // 打开连接\n        connection.start();\n        // 创建会话\n        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);\n        // 创建队列目标,并标识队列名称，消费者根据队列名称接收数据\n        Destination destination = session.createQueue(Que_Name);\n        // 创建消费者\n        MessageConsumer consumer = session.createConsumer(destination);\n        // 创建消费的监听\n        consumer.setMessageListener(new MessageListener() {\n            public void onMessage(Message message) {\n                TextMessage textMessage = (TextMessage) message;\n                try {\n                    System.out.println(\"消费的消息：\" + textMessage.getText());\n                } catch (JMSException e) {\n                    e.printStackTrace();\n                }\n            }\n        });\n    }\n}\n</code></pre>\n<p>测试查看web后台显示，5条消息被消费</p>\n<p><img class=\"wscnph\" src=\"\" /></p>\n<p>注意： activemq 自带负载均衡，当先启动两个队列（Queue）的消费者时，在启动生产者发出消息，此时的消息平均的被两个消费者消费。 并且消费者不会消费已经被消费的消息（即为已经出队的消息）</p>\n<hr />\n<h2>Topic模式</h2>\n<p>但是当有多个主题（Topic）订阅者时，发布者发布的消息，每个订阅者都会接收所有的消息。topic 更像是被广播的消息，但是缺点是不能接受已经发送过的消息。</p>\n<p><img src=\"https://note.youdao.com/yws/public/resource/39fb9a2ff543d021d619885806a15f05/xmlnote/2A7A19CFE5054A8AB948376B47DE97DE/12217\" alt=\"\" width=\"810\" height=\"485\" /></p>\n<p><span style=\"color: #ff0000;\">topic模式，先要有订阅者，生产者才有意义。</span></p>\n<hr />\n<h2>保证消息的可靠性</h2>\n<h3>持久化</h3>\n<pre class=\"language-markup\"><code>// 在队列为目的地的时候持久化消息\nmessageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);\n\n// 队列为目的地的非持久化消息\nmessageProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);</code></pre>\n<div>持久化的消息，服务器宕机后消息依旧存在，只是没有入队，当服务器再次启动，消息任就会被消费。</div>\n<div>但是非持久化的消息，服务器宕机后消息永远丢失。 而当你没有注明是否是持久化还是非持久化时，默认是持久化的消息。</div>\n<div>&nbsp;</div>\n<div>对于目的地为主题（topic）来说，默认就是非持久化的，让主题的订阅支持化的意义在于：对于订阅了公众号的人来说，当用户手机关机，在开机后任就可以接受到关注公众号之前发送的消息。</div>\n<div>&nbsp;</div>\n<div>\n<div>对于目的地为主题（topic）来说，默认就是非持久化的，让主题的订阅支持化的意义在于：对于订阅了公众号的人来说，当用户手机关机，在开机后任就可以接受到关注公众号之前发送的消息。</div>\n<div>代码实现：持久化topic 的消费者\n<pre class=\"language-java\"><code>     &hellip;&hellip;    // 前面代码相同，不复制了      \n        Topic topic = session.createTopic(TOPIC_NAME);\n        TopicSubscriber topicSubscriber = session.createDurableSubscriber(topic,\"remark...\");\n\n         //发布订阅\n        connection.start();\n\n        Message message = topicSubscriber.receive();// 一直等\n         while (null != message){\n             TextMessage textMessage = (TextMessage)message;\n             System.out.println(\" 收到的持久化 topic ：\"+textMessage.getText());\n             message = topicSubscriber.receive(3000L);    // 等1秒后meesage 为空，跳出循环，控制台关闭\n         }\n   &hellip;&hellip;​</code></pre>\n<div>持久化生产者</div>\n<div id=\"7510-1563505726307\" class=\"block-view code-view yne-code-theme-default\" data-language=\"javascript\" data-theme=\"default\">\n<div class=\"para-text\">\n<pre class=\"language-java\"><code>  &hellip;&hellip;  \n   \n        MessageProducer messageProducer = session.createProducer(topic);\n        // 6 通过messageProducer 生产 3 条 消息发送到消息队列中\n        // 设置持久化topic 在启动\n        messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT); \n        connection.start();\n        for (int i = 1; i &lt; 4 ; i++) {\n            // 7  创建字消息\n            TextMessage textMessage = session.createTextMessage(\"topic_name--\" + i);\n            // 8  通过messageProducer发布消息\n            messageProducer.send(textMessage);\n\n            MapMessage mapMessage = session.createMapMessage();\n            //    mapMessage.setString(\"k1\",\"v1\");\n            //     messageProducer.send(mapMessage);\n        }\n        // 9 关闭资源\n      &hellip;&hellip; </code></pre>\n<h3>事务</h3>\n<div>createSession的第一个参数为true 为开启事务，开启事务之后必须在将消息提交，才可以在队列中看到消息</div>\n<div id=\"9054-1563507508363\" class=\"block-view code-view yne-code-theme-default\" data-language=\"javascript\" data-theme=\"default\">\n<div class=\"para-text\">\n<pre class=\"language-java\"><code>Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);</code></pre>\n</div>\n</div>\n<div>提交：</div>\n<div id=\"6012-1563507584294\" class=\"block-view code-view yne-code-theme-default\" data-language=\"javascript\" data-theme=\"default\">\n<div class=\"para-text\">\n<pre class=\"language-java\"><code>session.commit(); </code></pre>\n<div>事务开启的意义在于，如果对于多条必须同批次传输的消息，可以使用事务，如果一条传输失败，可以将事务回滚，再次传输，保证数据的完整性。</div>\n<div>对于消息消费者来说，开启事务的话，可以避免消息被多次消费，以及后台和服务器数据的不一致性。举个栗子：</div>\n<div>如果消息消费的 createSession 设置为 ture ，但是没有 commit ，此时就会造成非常严重的后果，那就是在后台看来消息已经被消费，但是对于服务器来说并没有接收到消息被消费，此时就有可能被多次消费。</div>\n<h3>Acknowledge 签收 （俗称ack）</h3>\n<p>事务主要针对生产者，签收主要针对消费者。</p>\n<pre class=\"language-java\"><code>Session.AUTO_ACKNOWLEDGE      自动签收，默认\n\nSession.CLIENT_ACKNOWLEDGE     手动签收\n手动签收需要acknowledge   \ntextMessage.acknowledge();</code></pre>\n<p>对于开启事务时，设置手动签收和自动签收没有多大的意义，都默认自动签收，也就是说事务的优先级更高一些。</p>\n<hr />\n<h2>SpringBoot整合ActiveMQ</h2>\n<p><a href=\"https://github.com/lurenha/ActiveMQ\" target=\"_blank\" rel=\"noopener\">点击查看</a></p>\n<hr />\n<p><a href=\"https://note.youdao.com/ynoteshare1/index.html?id=39fb9a2ff543d021d619885806a15f05&amp;type=note\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>', 'JMS :  Java  消息中间件的服务接口规范，activemq 之上是 mq  ， 而 mq 之上是JMS 定义的消息规范 。 activemq 是mq 技术的一种理论实现（与之相类似的实现还有 Kafka  RabbitMQ  RockitMQ  ），而 JMS 是更上一级的规范。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 7, '2020-01-01 15:51:51', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (62, '113. 路径总和 II', '<p>给定一个二叉树和一个目标和，找到所有从根节点到叶子节点路径总和等于给定目标和的路径。</p>\n<p>说明:&nbsp;叶子节点是指没有子节点的节点。</p>\n<p>示例:<br />给定如下二叉树，以及目标和&nbsp;sum = 22，</p>\n<p><img class=\"wscnph\" src=\"\" /></p>\n<hr />\n<h2>回溯法</h2>\n<p>思路：沿着根节点往下寻找，保存中间的路径并一路求和，当到达叶子节点时：判断当前和是否等于sum？是：将这些路径存入结果集。不是：返回。</p>\n<pre class=\"language-java\"><code> /**\n     * Definition for a binary tree node.\n     * public class TreeNode {\n     * int val;\n     * TreeNode left;\n     * TreeNode right;\n     * TreeNode(int x) { val = x; }\n     * }\n     */\n    class Solution {\n        public List&lt;List&lt;Integer&gt;&gt; pathSum(TreeNode root, int sum) {\n            List&lt;List&lt;Integer&gt;&gt; lists = new ArrayList&lt;&gt;();\n            backstack(root, sum, 0, lists, new ArrayList&lt;&gt;());\n            return lists;\n        }\n\n        void backstack(TreeNode tem, int sum, int res, List&lt;List&lt;Integer&gt;&gt; lists, List&lt;Integer&gt; small) {\n            if (tem == null)\n                return;\n            res += tem.val;//res记录当前路径上的和\n            small.add(tem.val);//回溯入\n            if (tem.left == null &amp;&amp; tem.right == null &amp;&amp; sum == res) {//这条路径是结果之一\n                lists.add(new ArrayList&lt;&gt;(small));\n            } else {\n                backstack(tem.left, sum, res, lists, small);\n                backstack(tem.right, sum, res, lists, small);\n            }\n            small.remove(small.size() - 1);//回溯出\n        }\n    }</code></pre>\n<hr />\n<p><img class=\"wscnph\" src=\"\" /></p>', '给定一个二叉树和一个目标和，找到所有从根节点到叶子节点路径总和等于给定目标和的路径。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 2, '2020-01-06 23:28:26', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (63, 'mysql中InnoDB 主键问题', '<h3>&lt;高性能MYSQL&gt;原话:</h3>\n<p><img src=\"https://images2015.cnblogs.com/blog/268981/201510/268981-20151009212711221-1004867019.jpg\" alt=\"\" /></p>\n<hr />\n<h3>InnoDB引擎表是基于B+树的索引组织表</h3>\n<p>如下这个图就很好的说明了B+的特点</p>\n<p><img src=\"http://img.mp.sohu.com/upload/20170713/d8ae1b14e9bf4b1890146eb803ee9795_th.png\" alt=\"\" /></p>\n<p><strong>B+树的特征：</strong></p>\n<p>1.有k个子树的中间节点包含有k个元素（B树中是k-1个元素），每个元素不保存数据，只用来索引，所有数据都保存在叶子节点。</p>\n<p>2.所有的叶子结点中包含了全部元素的信息，及指向含这些元素记录的指针，且叶子结点本身依关键字的大小自小而大顺序链接。</p>\n<p>3.所有的中间节点元素都同时存在于子节点，在子节点元素中是最大（或最小）元素。</p>\n<p><strong>B+树的优势：</strong></p>\n<p>1.单一节点存储更多的元素，使得查询的IO次数更少。</p>\n<p>2.所有查询都要查找到叶子节点，查询性能稳定。</p>\n<p>3.所有叶子节点形成有序链表，便于范围查询。</p>\n<hr />\n<h3>使用自增主键的好处</h3>\n<p>那么每次插入新的记录，记录就会顺序添加到当前索引节点的后续位置，当一页写满，就会自动开辟一个新的页</p>\n<h3>使用非自增主键坏处</h3>\n<p>由于每次插入主键的值近似于随机，因此每次新纪录都要被插到现有索引页得中间某个位置，此时MySQL不得不为了将新记录插到合适位置而移动数据，甚至目标页面可能已经被回写到磁盘上而从缓存中清掉，此时又要从磁盘上读回来，这增加了很多开销，同时频繁的移动、分页操作造成了大量的碎片，得到了不够紧凑的索引结构，后续不得不通过OPTIMIZE TABLE来重建表并优化填充页面。</p>\n<h3>总结</h3>\n<p>如果InnoDB表的数据写入顺序能和B+树索引的叶子节点顺序一致的话，这时候存取效率是最高的。也就是下面这几种情况的存取效率最高：</p>\n<ul class=\"list-paddingleft-2\">\n<li>\n<p>使用自增列(INT/BIGINT类型)做主键，这时候写入顺序是自增的，和B+数叶子节点分裂顺序一致；</p>\n</li>\n<li>\n<p>该表不指定自增列做主键，同时也没有可以被选为主键的唯一索引(上面的条件)，这时候InnoDB会选择内置的ROWID作为主键，写入顺序和ROWID增长顺序一致；</p>\n</li>\n<li>\n<p>如果一个InnoDB表又没有显示主键，又有可以被选择为主键的唯一索引，但该唯一索引可能不是递增关系时(例如字符串、UUID、多字段联合唯一索引的情况)，该表的存取效率就会比较差。</p>\n</li>\n</ul>\n<hr />\n<p><a href=\"https://mp.weixin.qq.com/s/0lpkoaTI8FDAsH6EwwEBfg\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>', '为什么mysql建议用自增id做主键', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 4, '2020-01-14 13:17:42', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (64, '139. 单词拆分', '<p>给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict，判定&nbsp;s 是否可以被空格拆分为一个或多个在字典中出现的单词。</p>\n<p>说明：</p>\n<p>拆分时可以重复使用字典中的单词。<br />你可以假设字典中没有重复的单词。</p>\n<pre class=\"language-markup\"><code>示例 1：输入: s = \"leetcode\", wordDict = [\"leet\", \"code\"]\n输出: true\n解释: 返回 true 因为 \"leetcode\" 可以被拆分成 \"leet code\"。</code></pre>\n<pre class=\"language-markup\"><code>示例 2：输入: s = \"applepenapple\", wordDict = [\"apple\", \"pen\"]\n输出: true\n解释: 返回 true 因为 \"applepenapple\" 可以被拆分成 \"apple pen apple\"。  注意你可以重复使用字典中的单词。</code></pre>\n<pre class=\"language-markup\"><code>示例 3：输入: s = \"catsandog\", wordDict = [\"cats\", \"dog\", \"sand\", \"and\", \"cat\"]\n输出: false</code></pre>\n<hr />\n<h2>分析：动态规划</h2>\n<p>设dp[n]：代表字符串下标为n时，是否可以被拆分。</p>\n<p>动态转移方程：dp[i]=dp[i-word.length()]&amp;&amp;wordList.has(word)] //word为 i&mdash;（word.length()-i） 构成的单词</p>\n<pre class=\"language-java\"><code>    class Solution {\n        public boolean wordBreak(String s, List&lt;String&gt; wordDict) {\n            boolean[] dp=new boolean[s.length()+1];\n            dp[0]=true;\n            for (int i = 1; i &lt; dp.length; i++) {\n                for (int j = 0; j &lt; i; j++) {\n                    if(dp[j]&amp;&amp;  wordDict.contains(s.substring(j,i))){\n                        dp[i]=true;\n                        break;\n                    }\n                }\n            }\n            return dp[s.length()];\n        }\n    }</code></pre>\n<h3>优化</h3>\n<p>记录单词表里最长单词的长度.从第二重循环时判断如果当前下标i已经大于最长单词了那么j从0肯定无法找到一个单词匹配,则从(i-最长单词长度)开始寻找.</p>\n<pre class=\"language-java\"><code>class Solution {\n   public boolean wordBreak(String s, List&lt;String&gt; wordDict) {\n       boolean dp[]=new boolean[s.length()+1];\n       dp[0]=true;\n       int maxworDict=0;\n       int begin;\n       for(String w:wordDict){\n           maxworDict=Integer.max(maxworDict,w.length());\n       }\n\n        for(int i=1;i&lt;dp.length;i++){\n            begin =i-maxworDict&gt;0 ? i-maxworDict:0;\n            for(int j=begin;j&lt;i;j++){\n                if(dp[j]&amp;&amp;wordDict.contains(s.substring(j,i))){\n                    dp[i]=true;\n                    break;\n                }\n            }\n        }\n        return dp[s.length()];\n    }\n}</code></pre>\n<hr />\n<p><img class=\"wscnph\" src=\"\" /></p>', '给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict，判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 2, '2020-01-17 14:39:44', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (65, '152. 乘积最大子序列', '<p>给定一个整数数组&nbsp;<code>nums</code>&nbsp;，找出一个序列中乘积最大的连续子序列（该序列至少包含一个数）。</p>\n<pre class=\"language-markup\"><code>示例 1:\n\n输入: [2,3,-2,4]\n输出: 6\n解释: 子数组 [2,3] 有最大乘积 6。</code></pre>\n<pre class=\"language-markup\"><code>示例 2:\n\n输入: [-2,0,-1]\n输出: 0\n解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。</code></pre>\n<h3><br />动态规划</h3>\n<ul>\n<li>遍历数组时计算当前最大值，不断更新</li>\n<li>令imax为当前最大值，则当前最大值为 imax = max(imax * nums[i], nums[i])</li>\n<li>由于存在负数，那么会导致最大的变最小的，最小的变最大的。因此还需要维护当前最小值imin，imin = min(imin * nums[i], nums[i])</li>\n<li>当负数出现时则imax与imin进行交换再进行下一步计算</li>\n<li>时间复杂度：O(n)O(n)</li>\n</ul>\n<pre class=\"language-java\"><code>class Solution {\n    public int maxProduct(int[] nums) {\n        int max = Integer.MIN_VALUE, imax = 1, imin = 1;\n        for(int i=0; i&lt;nums.length; i++){\n            if(nums[i] &lt; 0){ \n              int tmp = imax;\n              imax = imin;\n              imin = tmp;\n            }\n            imax = Math.max(imax*nums[i], nums[i]);\n            imin = Math.min(imin*nums[i], nums[i]);\n            \n            max = Math.max(max, imax);\n        }\n        return max;\n    }\n}\n</code></pre>\n<hr />\n<p><img class=\"wscnph\" src=\"\" /><br /><br /></p>\n<hr />\n<p><a href=\"https://leetcode-cn.com/problems/maximum-product-subarray/solution/hua-jie-suan-fa-152-cheng-ji-zui-da-zi-xu-lie-by-g/\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>', '给定一个整数数组 nums ，找出一个序列中乘积最大的连续子序列（该序列至少包含一个数）。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 2, '2020-01-17 16:05:44', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (66, '279. 完全平方数', '<p>给定正整数&nbsp;<em>n</em>，找到若干个完全平方数（比如&nbsp;<code>1, 4, 9, 16, ...</code>）使得它们的和等于<em>&nbsp;n</em>。你需要让组成和的完全平方数的个数最少。</p>\n<pre class=\"language-markup\"><code>示例 1:\n\n输入: n = 12\n输出: 3 \n解释: 12 = 4 + 4 + 4.</code></pre>\n<pre class=\"language-markup\"><code>示例 2:\n\n输入: n = 13\n输出: 2\n解释: 13 = 4 + 9.</code></pre>\n<hr />\n<h3>分析:动态规划</h3>\n<p>我们设方程dp[n]：代表到数字n所需要的最少平方数的个数</p>\n<p>转移方程：dp[i]=Min（dp[1&sup2;]+dp[(i-1&sup2;)]，dp[2&sup2;]+dp[(i-2&sup2;)，dp[3&sup2;]+dp[(i-3&sup2;)，dp[4&sup2;]+dp[(i-4&sup2;)......]） [注：i&gt;=1&sup2;,2&sup2;,3&sup2;,4&sup2;...]</p>\n<pre class=\"language-java\"><code>    class Solution {\n        public int numSquares(int n) {\n            int[] dp=new int[n+1];\n            dp[0]=1;\n            int iMin;\n            for (int i = 1; i &lt;= n; i++) {\n                iMin=Integer.MAX_VALUE;\n                for (int j = 1; j*j &lt;=i ; j++) {\n                    iMin=Math.min(dp[i-j*j]+dp[j*j],iMin);\n                }\n                dp[i]=iMin;\n            }\n            return dp[n];\n        }\n    }</code></pre>\n<p><code>我们发现dp[j*j](dp[1&sup2;],dp[2&sup2;],dp[3&sup2;]...)都是等于1的,所以可以优化为 iMin=Math.min(dp[i-j*j]+1,iMin);//此时:dp[0]=0</code></p>\n<p>说明下为什么第一种 dp[0]=1，第二种dp[0]=0.</p>\n<p>第一种 比如当i=9, j=3时，dp[i-j*j]+dp[j*j]=dp[0]+dp[9],此时dp[9]是数组给的初值0，我们把dp[0]=1作为了最少需要的平方数。</p>\n<p>第二种 比如当i=9, j=3时，dp[i-j*j]+1=dp[0]+1,我们需要让dp[0]=0，满足条件。</p>\n<pre class=\"language-java\"><code>    class Solution {\n        public int numSquares(int n) {\n            int[] dp=new int[n+1];\n            //dp[0]=1;\n            int iMin;\n            for (int i = 1; i &lt;= n; i++) {\n                iMin=Integer.MAX_VALUE;\n                for (int j = 1; j*j &lt;=i ; j++) {\n                    iMin=Math.min(dp[i-j*j]+1,iMin);\n                }\n                dp[i]=iMin;\n            }\n            return dp[n];\n        }\n    }</code></pre>\n<p><code></code></p>\n<p><code></code></p>\n<hr />\n<p><img class=\"wscnph\" src=\"\" /></p>', '给定正整数 n，找到若干个完全平方数（比如 1, 4, 9, 16, ...）使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 2, '2020-01-18 14:40:56', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (67, '322. 零钱兑换', '<p>给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额，返回&nbsp;-1。</p>\n<pre class=\"language-markup\"><code>示例 1:\n\n输入: coins = [1, 2, 5], amount = 11\n输出: 3 \n解释: 11 = 5 + 5 + 1</code></pre>\n<pre class=\"language-markup\"><code>示例 2:\n\n输入: coins = [2], amount = 3\n输出: -1</code></pre>\n<hr />\n<h3>分析：动态规划</h3>\n<p><code>设dp[n]</code>代表到金额<code>i需要的最少硬币数</code></p>\n<p>动态转移方程：<span style=\"font-family: monospace;\"><span style=\"background-color: #bfe6ff;\">dp[i]=Min(dp[i-coins[0]]+1,<span style=\"font-family: monospace;\">dp[i-coins[1]+1,dp[i-coins[2]]+1......</span>)</span></span></p>\n<pre class=\"language-java\"><code>   class Solution {\n        public int coinChange(int[] coins, int amount) {\n            int[] dp=new int[amount+1];\n            dp[0]=0;\n            for (int i = 1; i &lt;= amount; i++) {\n                dp[i]=Integer.MAX_VALUE-1;//防止+1溢出\n            }\n          \n            for (int i = 0; i &lt;= amount; i++) {\n                for (int j = 0; j &lt; coins.length; j++) {\n                    if(i&gt;=coins[j]){\n                         dp[i]=Math.min(dp[i],dp[i-coins[j]]+1);\n                    }\n                }         \n            }\n            if(dp[amount]==Integer.MAX_VALUE-1)\n                 return -1;\n            return dp[amount];\n        }\n    }</code></pre>\n<hr />\n<p><img class=\"wscnph\" src=\"\" /></p>\n<p><code></code></p>', '给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额，返回 -1。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 2, '2020-01-18 15:19:36', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (68, '一个方法团灭 6 道股票问题', '<p><a href=\"https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/solution/yi-ge-fang-fa-tuan-mie-6-dao-gu-piao-wen-ti-by-l-3/\" target=\"_blank\" rel=\"noopener\">查看原文</a></p>\n<h3><code>一、穷举框架首先，还是一样的思路：如何穷举？</code></h3>\n<p>递归其实是符合我们思考的逻辑的，一步步推进，遇到无法解决的就丢给递归，一不小心就做出来了，可读性还很好。缺点就是一旦出错，你也不容易找到错误出现的原因。比如上篇文章的递归解法，肯定还有计算冗余，但确实不容易找到。</p>\n<p>而这里，我们不用递归思想进行穷举，而是利用「状态」进行穷举。我们具体到每一天，看看总共有几种可能的「状态」，再找出每个「状态」对应的「选择」。我们要穷举所有「状态」，穷举的目的是根据对应的「选择」更新状态。听起来抽象，你只要记住「状态」和「选择」两个词就行，下面实操一下就很容易明白了。</p>\n<pre class=\"language-java\"><code>for 状态1 in 状态1的所有取值：\n    for 状态2 in 状态2的所有取值：\n        for ...\n            dp[状态1][状态2][...] = 择优(选择1，选择2...)\n\n</code></pre>\n<p>比如说这个问题，每天都有三种「选择」：买入、卖出、无操作，我们用 buy, sell, rest 表示这三种选择。但问题是，并不是每天都可以任意选择这三种选择的，因为 sell 必须在 buy 之后，buy 必须在 sell 之后。那么 rest 操作还应该分两种状态，一种是 buy 之后的 rest（持有了股票），一种是 sell 之后的 rest（没有持有股票）。而且别忘了，我们还有交易次数 k 的限制，就是说你 buy 还只能在 k &gt; 0 的前提下操作。</p>\n<p>很复杂对吧，不要怕，我们现在的目的只是穷举，你有再多的状态，老夫要做的就是一把梭全部列举出来。这个问题的「状态」有三个，第一个是天数，第二个是允许交易的最大次数，第三个是当前的持有状态（即之前说的 rest 的状态，我们不妨用 1 表示持有，0 表示没有持有）。然后我们用一个三维数组就可以装下这几种状态的全部组合：</p>\n<pre class=\"language-java\"><code>dp[i][k][0 or 1]\n0 &lt;= i &lt;= n-1, 1 &lt;= k &lt;= K\nn 为天数，大 K 为最多交易数\n此问题共 n &times; K &times; 2 种状态，全部穷举就能搞定。\n\nfor 0 &lt;= i &lt; n:\n    for 1 &lt;= k &lt;= K:\n        for s in {0, 1}:\n            dp[i][k][s] = max(buy, sell, rest)\n</code></pre>\n<p>而且我们可以用自然语言描述出每一个状态的含义，比如说 dp[3][2][1] 的含义就是：今天是第三天，我现在手上持有着股票，至今最多进行 2 次交易。再比如 dp[2][3][0] 的含义：今天是第二天，我现在手上没有持有股票，至今最多进行 3 次交易。很容易理解，对吧？</p>\n<p>我们想求的最终答案是 dp[n - 1][K][0]，即最后一天，最多允许 K 次交易，最多获得多少利润。读者可能问为什么不是 dp[n - 1][K][1]？因为 [1] 代表手上还持有股票，[0] 表示手上的股票已经卖出去了，很显然后者得到的利润一定大于前者。</p>\n<p>记住如何解释「状态」，一旦你觉得哪里不好理解，把它翻译成自然语言就容易理解了。</p>\n<h3><code>二、状态转移框架</code></h3>\n<p>现在，我们完成了「状态」的穷举，我们开始思考每种「状态」有哪些「选择」，应该如何更新「状态」。只看「持有状态」，可以画个状态转移图。</p>\n<p><img src=\"https://pic.leetcode-cn.com/c4eb5f0aa4daf7bef4b3b8af95129bb7394ec58e1ba7b191d9104bbd8ff1ccb3-40198bf2f6894018328b250b772b4a17724a983f99ba359b798a289733bffcbc-file_1559885188422-1.png\" alt=\"\" /></p>\n<pre class=\"language-java\"><code>通过这个图可以很清楚地看到，每种状态（0 和 1）是如何转移而来的。根据这个图，我们来写一下状态转移方程：\n\ndp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])\n              max(   选择 rest  ,           选择 sell      )\n\n解释：今天我没有持有股票，有两种可能：\n要么是我昨天就没有持有，然后今天选择 rest，所以我今天还是没有持有；\n要么是我昨天持有股票，但是今天我 sell 了，所以我今天没有持有股票了。\n\ndp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])\n              max(   选择 rest  ,           选择 buy         )\n\n解释：今天我持有着股票，有两种可能：\n要么我昨天就持有着股票，然后今天选择 rest，所以我今天还持有着股票；\n要么我昨天本没有持有，但今天我选择 buy，所以今天我就持有股票了。\n</code></pre>\n<p><code></code></p>\n<p>这个解释应该很清楚了，如果 buy，就要从利润中减去 prices[i]，如果 sell，就要给利润增加 prices[i]。今天的最大利润就是这两种可能选择中较大的那个。而且注意 k 的限制，我们在选择 buy 的时候，把 k 减小了 1，很好理解吧，当然你也可以在 sell 的时候减 1，一样的。</p>\n<p>现在，我们已经完成了动态规划中最困难的一步：状态转移方程。如果之前的内容你都可以理解，那么你已经可以秒杀所有问题了，只要套这个框架就行了。不过还差最后一点点，就是定义 base case，即最简单的情况。</p>\n<pre class=\"language-markup\"><code>dp[-1][k][0] = 0\n解释：因为 i 是从 0 开始的，所以 i = -1 意味着还没有开始，这时候的利润当然是 0 。\ndp[-1][k][1] = -infinity\n解释：还没开始的时候，是不可能持有股票的，用负无穷表示这种不可能。\ndp[i][0][0] = 0\n解释：因为 k 是从 1 开始的，所以 k = 0 意味着根本不允许交易，这时候利润当然是 0 。\ndp[i][0][1] = -infinity\n解释：不允许交易的情况下，是不可能持有股票的，用负无穷表示这种不可能。\n\n</code></pre>\n<pre class=\"language-java\"><code>把上面的状态转移方程总结一下：\n\nbase case：\ndp[-1][k][0] = dp[i][0][0] = 0\ndp[-1][k][1] = dp[i][0][1] = -infinity\n\n状态转移方程：\ndp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])\ndp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])</code></pre>\n<p>读者可能会问，这个数组索引是 -1 怎么编程表示出来呢，负无穷怎么表示呢？这都是细节问题，有很多方法实现。现在完整的框架已经完成，下面开始具体化。</p>\n<h3>三、秒杀题目</h3>\n<p><strong>第一题，k = 1</strong></p>\n<p>直接套状态转移方程，根据 base case，可以做一些化简：</p>\n<pre class=\"language-java\"><code>dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])\ndp[i][1][1] = max(dp[i-1][1][1], dp[i-1][0][0] - prices[i]) \n            = max(dp[i-1][1][1], -prices[i])\n解释：k = 0 的 base case，所以 dp[i-1][0][0] = 0。\n\n现在发现 k 都是 1，不会改变，即 k 对状态转移已经没有影响了。\n可以进行进一步化简去掉所有 k：\ndp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])\ndp[i][1] = max(dp[i-1][1], -prices[i])\n直接写出代码：\n\nint n = prices.length;\nint[][] dp = new int[n][2];\nfor (int i = 0; i &lt; n; i++) {\n    dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);\n    dp[i][1] = Math.max(dp[i-1][1], -prices[i]);\n}\nreturn dp[n - 1][0];\n\n\n</code></pre>\n<pre class=\"language-java\"><code>显然 i = 0 时 dp[i-1] 是不合法的。这是因为我们没有对 i 的 base case 进行处理。可以这样处理：\n\nfor (int i = 0; i &lt; n; i++) {\n    if (i - 1 == -1) {\n        dp[i][0] = 0;\n        // 解释：\n        //   dp[i][0] \n        // = max(dp[-1][0], dp[-1][1] + prices[i])\n        // = max(0, -infinity + prices[i]) = 0\n        dp[i][1] = -prices[i];\n        //解释：\n        //   dp[i][1] \n        // = max(dp[-1][1], dp[-1][0] - prices[i])\n        // = max(-infinity, 0 - prices[i]) \n        // = -prices[i]\n        continue;\n    }\n    dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);\n    dp[i][1] = Math.max(dp[i-1][1], -prices[i]);\n}\nreturn dp[n - 1][0];</code></pre>\n<p><strong>第二题，k = +infinity</strong></p>\n<pre class=\"language-java\"><code>如果 k 为正无穷，那么就可以认为 k 和 k - 1 是一样的。可以这样改写框架：\n\ndp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])\ndp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])\n            = max(dp[i-1][k][1], dp[i-1][k][0] - prices[i])\n\n我们发现数组中的 k 已经不会改变了，也就是说不需要记录 k 这个状态了：\ndp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])\ndp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])</code></pre>\n<pre class=\"language-java\"><code>直接翻译成代码：\n\nint maxProfit_k_inf(int[] prices) {\n    int n = prices.length;\n    int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;\n    for (int i = 0; i &lt; n; i++) {\n        int temp = dp_i_0;\n        dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);\n        dp_i_1 = Math.max(dp_i_1, temp - prices[i]);\n    }\n    return dp_i_0;\n}</code></pre>\n<p><strong>第三题，k = +infinity with cooldown</strong></p>\n<p>&nbsp;</p>\n<pre class=\"language-java\"><code>每次 sell 之后要等一天才能继续交易。只要把这个特点融入上一题的状态转移方程即可：\n\ndp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])\ndp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i])\n解释：第 i 天选择 buy 的时候，要从 i-2 的状态转移，而不是 i-1 。\n</code></pre>\n<pre class=\"language-java\"><code>翻译成代码：\n\nint maxProfit_with_cool(int[] prices) {\n    int n = prices.length;\n    int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;\n    int dp_pre_0 = 0; // 代表 dp[i-2][0]\n    for (int i = 0; i &lt; n; i++) {\n        int temp = dp_i_0;\n        dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);\n        dp_i_1 = Math.max(dp_i_1, dp_pre_0 - prices[i]);\n        dp_pre_0 = temp;\n    }\n    return dp_i_0;\n}</code></pre>\n<p><strong>第四题，k = +infinity with fee</strong></p>\n<pre class=\"language-java\"><code>每次交易要支付手续费，只要把手续费从利润中减去即可。改写方程：\n\ndp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])\ndp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee)\n解释：相当于买入股票的价格升高了。\n在第一个式子里减也是一样的，相当于卖出股票的价格减小了。\n</code></pre>\n<pre class=\"language-java\"><code>直接翻译成代码：\n\nint maxProfit_with_fee(int[] prices, int fee) {\n    int n = prices.length;\n    int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;\n    for (int i = 0; i &lt; n; i++) {\n        int temp = dp_i_0;\n        dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);\n        dp_i_1 = Math.max(dp_i_1, temp - prices[i] - fee);\n    }\n    return dp_i_0;\n}</code></pre>\n<p><strong>第五题，k = 2</strong></p>\n<p>&nbsp;</p>\n<pre class=\"language-java\"><code>k = 2 和前面题目的情况稍微不同，因为上面的情况都和 k 的关系不太大。要么 k 是正无穷，状态转移和 k 没关系了；要么 k = 1，跟 k = 0 这个 base case 挨得近，最后也没有存在感。\n\n这道题 k = 2 和后面要讲的 k 是任意正整数的情况中，对 k 的处理就凸显出来了。我们直接写代码，边写边分析原因。\n\n原始的动态转移方程，没有可化简的地方\ndp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])\ndp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])</code></pre>\n<pre class=\"language-java\"><code>int max_k = 2;\nint[][][] dp = new int[n][max_k + 1][2];\nfor (int i = 0; i &lt; n; i++) {\n    for (int k = max_k; k &gt;= 1; k--) {\n        if (i - 1 == -1) { \n            /* 处理 base case */\n            dp[i][k][0] = 0;\n            dp[i][k][1] = -prices[i];\n            continue;\n        }\n        dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);\n        dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);\n    }\n}\n// 穷举了 n &times; max_k &times; 2 个状态，正确。\nreturn dp[n - 1][max_k][0];</code></pre>\n<p><strong>第六题，k = any integer</strong></p>\n<p>有了上一题 k = 2 的铺垫，这题应该和上一题的第一个解法没啥区别。但是出现了一个超内存的错误，原来是传入的 k 值会非常大，dp 数组太大了。现在想想，交易次数 k 最多有多大呢？</p>\n<p>一次交易由买入和卖出构成，至少需要两天。所以说有效的限制 k 应该不超过 n/2，如果超过，就没有约束作用了，相当于 k = +infinity。这种情况是之前解决过的。</p>\n<p>直接把之前的代码重用：</p>\n<pre class=\"language-java\"><code>int maxProfit_k_any(int max_k, int[] prices) {\n    int n = prices.length;\n    if (max_k &gt; n / 2) \n        return maxProfit_k_inf(prices);\n\n    int[][][] dp = new int[n][max_k + 1][2];\n    for (int i = 0; i &lt; n; i++) \n        for (int k = max_k; k &gt;= 1; k--) {\n            if (i - 1 == -1) { \n                /* 处理 base case */\n                dp[i][k][0] = 0;\n                dp[i][k][1] = -prices[i];\n                continue;\n            }\n            dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);\n            dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);     \n        }\n    return dp[n - 1][max_k][0];\n}</code></pre>\n<p>&nbsp;</p>', '用一个状态转移方程秒杀了 6 道股票买卖问题', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 2, '2020-01-19 17:27:17', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (69, '47. 全排列 II', '<h2>47. 全排列 II</h2>\n<p>给定一个可<span style=\"color: #ff0000;\">包含重复数字</span>的序列，返回所有<span style=\"color: #ff0000;\">不重复</span>的全排列。</p>\n<p>示例:</p>\n<p>输入: [1,1,2]<br />输出:<br />[<br />[1,1,2],<br />[1,2,1],<br />[2,1,1]<br />]</p>\n<hr />\n<p>分析：在全排列的基础上加上了限制条件不重复。可以直接用全排列的代码进行剪纸操作。如图</p>\n<p><img class=\"wscnph\" src=\"\" /></p>\n<p>我们发现在每层计算中，只需要计算第一次出现的数字，再出现重复的数字无需运算。</p>\n<p>所以我们可以在每层中使用Set进行去重。</p>\n<hr />\n<pre class=\"language-java\"><code>class Solution {\n    public List&lt;List&lt;Integer&gt;&gt; permuteUnique(int[] nums) {\n        List&lt;List&lt;Integer&gt;&gt; res=new ArrayList&lt;&gt;();\n        backstack(nums,0,nums.length,res);\n        return res;\n    }\n\n    private void backstack(int[] nums,int begin,int end, List&lt;List&lt;Integer&gt;&gt; res){\n        if(begin==end){\n            List&lt;Integer&gt; tem = new ArrayList&lt;Integer&gt;();\n            for (int n = 0; n &lt; nums.length; n++) {\n                tem.add(nums[n]);\n            }\n            res.add(tem);\n            return;\n        }\n        \n        Set&lt;Integer&gt; set=new HashSet&lt;&gt;();\n        for (int i = begin; i &lt;end; i++) {//begin-end 之间的全排列\n           if(set.contains(nums[i]))//去重\n                continue;\n            set.add(nums[i]);\n            swap(nums,i,begin);\n            backstack(nums,begin+1,end,res);\n            swap(nums,i,begin);\n\n        }\n    }\n\n    private  void swap(int[] nums, int x, int y) {\n        int a = nums[x];\n        nums[x] = nums[y];\n        nums[y] = a;\n    }\n}</code></pre>\n<hr />\n<p><img class=\"wscnph\" src=\"\" /></p>', '47. 全排列 II', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 2, '2020-01-31 15:29:01', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (70, 'springboot属性注入', '<h3>方式一</h3>\n<p>通过@Value为属性注入值。</p>\n<p>1.使用application.properties文件，添加配置信息。</p>\n<pre class=\"language-markup\"><code>jdbc.driverClassName=com.mysql.jdbc.Driver\n\njdbc.url=jdbc:mysql://127.0.0.1:3306/leyou\n\njdbc.username=root\n\njdbc.password=123</code></pre>\n<p>2.创建JdbcConfiguration类，使用spring中的value注解对每个属性进行注入,用bean注解将返回值添加到容器中</p>\n<pre class=\"language-markup\"><code>@Configuration\n\n@PropertySource(\"classpath:jdbc.properties\")\n\npublic class JdbcConfiguration {\n\n \n\n    @Value(\"${jdbc.url}\")\n\n    String url;\n\n    @Value(\"${jdbc.driverClassName}\")\n\n    String driverClassName;\n\n    @Value(\"${jdbc.username}\")\n\n    String username;\n\n    @Value(\"${jdbc.password}\")\n\n    String password;\n\n \n\n    @Bean\n\n    public DataSource dataSource() {\n\n        DruidDataSource dataSource = new DruidDataSource();\n\n        dataSource.setUrl(url);\n\n        dataSource.setDriverClassName(driverClassName);\n\n        dataSource.setUsername(username);\n\n        dataSource.setPassword(password);\n\n        return dataSource;\n\n    }\n\n}\n</code></pre>\n<ul>\n<li>@Configuration：声明JdbcConfiguration是一个配置类。</li>\n<li>@PropertySource：指定属性文件的路径是:classpath:jdbc.properties</li>\n<li>通过@Value为属性注入值。</li>\n<li>通过@Bean将 dataSource()方法声明为一个注册Bean的方法，Spring会自动调用该方法，将方法的返回值加入Spring容器中。相当于以前的bean标签</li>\n<li>然后就可以在任意位置通过@Autowired注入DataSource了！</li>\n</ul>\n<hr />\n<h3>方式二（常用）</h3>\n<p>使用@ConfigurationProperties注入</p>\n<p>需求：我们可能在系统中需要用到多种文件存储模式（服务器本地存储，七牛云对象存储，阿里云等）</p>\n<p>1.配置文件application.yml是这样的（服务器本地存储，七牛云对象存储）</p>\n<pre class=\"language-markup\"><code>myapplication:\n  # 对象存储配置\n  storage:\n    # 当前工作的对象存储模式，分别是local,qiniu\n    active: local\n\n    # 本地对象存储配置信息\n    local:\n      storagePath: storage\n      address: http://localhost:8080/xxx/\n\n    # 七牛云对象存储配置信息\n    qiniu:\n      endpoint: http://s3-cn-north-1.qiniucs.com\n      accessKey: xxxxxxxxxxxxxxx\n      secretKey: xxxxxxxxxxxxxxx\n      bucketName: pengcloud01\n\n </code></pre>\n<p>&nbsp;</p>\n<p>2.使用@ConfigurationProperties注解将配置属性注入</p>\n<pre class=\"language-java\"><code>/***\n * 将配置文件（myapplication.storage）转换成对象\n */\n@ConfigurationProperties(prefix = \"myapplication.storage\")\npublic class StorageProperties {\n    private String active;\n    private Local local;\n    private Qiniu qiniu;\n\n    public String getActive() {\n        return active;\n    }\n\n    public void setActive(String active) {\n        this.active = active;\n    }\n\n    public Local getLocal() {\n        return local;\n    }\n\n    public void setLocal(Local local) {\n        this.local = local;\n    }\n\n    public Qiniu getQiniu() {\n        return qiniu;\n    }\n\n    public void setQiniu(Qiniu qiniu) {\n        this.qiniu = qiniu;\n    }\n\n\n    public static class Local {\n        private String address;\n        private String storagePath;\n\n        public String getAddress() {\n            return address;\n        }\n\n        public void setAddress(String address) {\n            this.address = address;\n        }\n\n        public String getStoragePath() {\n            return storagePath;\n        }\n\n        public void setStoragePath(String storagePath) {\n            this.storagePath = storagePath;\n        }\n    }\n\n    public static class Qiniu {\n        private String endpoint;\n        private String accessKey;\n        private String secretKey;\n        private String bucketName;\n\n        public String getEndpoint() {\n            return endpoint;\n        }\n\n        public void setEndpoint(String endpoint) {\n            this.endpoint = endpoint;\n        }\n\n        public String getAccessKey() {\n            return accessKey;\n        }\n\n        public void setAccessKey(String accessKey) {\n            this.accessKey = accessKey;\n        }\n\n        public String getSecretKey() {\n            return secretKey;\n        }\n\n        public void setSecretKey(String secretKey) {\n            this.secretKey = secretKey;\n        }\n\n        public String getBucketName() {\n            return bucketName;\n        }\n\n        public void setBucketName(String bucketName) {\n            this.bucketName = bucketName;\n        }\n    }\n\n\n\n   \n\n}\n</code></pre>\n<p>3.编写配置类根据具体active选择对应的实现，最终我们就可以注入@Autowired StorageService 进行使用了</p>\n<pre class=\"language-java\"><code>/***\n * 根据配置文件active注入对应的实现类\n */\n@Configuration\n@EnableConfigurationProperties(StorageProperties.class)//要用到的属性类\npublic class StorageAutoConfiguration {\n\n    //可以使用注解注入\n    // @Autowired\n    //private StorageProperties properties;\n\n    //也可以使用构造函数传入\n    private final StorageProperties properties;\n    public StorageAutoConfiguration(StorageProperties properties) {\n        this.properties = properties;\n    }\n\n    @Bean\n    public StorageService storageService() {\n        StorageService storageService = new StorageService();\n        String active = this.properties.getActive();\n        storageService.setActive(active);\n        if (active.equals(\"local\")) {\n            storageService.setStorage(localStorage());\n        } else if (active.equals(\"qiniu\")) {\n            storageService.setStorage(qiniuStorage());\n        } else {\n            throw new RuntimeException(\"当前存储模式 \" + active + \" 不支持\");\n        }\n        return storageService;\n    }\n\n    @Bean\n    public LocalStorage localStorage() {\n        LocalStorage localStorage = new LocalStorage();\n        StorageProperties.Local local = this.properties.getLocal();\n        localStorage.setAddress(local.getAddress());\n        localStorage.setStoragePath(local.getStoragePath());\n        return localStorage;\n    }\n\n    @Bean\n    public QiniuStorage qiniuStorage() {\n        QiniuStorage qiniuStorage = new QiniuStorage();\n        StorageProperties.Qiniu qiniu = this.properties.getQiniu();\n        qiniuStorage.setAccessKey(qiniu.getAccessKey());\n        qiniuStorage.setSecretKey(qiniu.getSecretKey());\n        qiniuStorage.setBucketName(qiniu.getBucketName());\n        qiniuStorage.setEndpoint(qiniu.getEndpoint());\n        return qiniuStorage;\n    }\n\n\n\n}</code></pre>\n<p>注：StorageService是提供存储服务的代理类，具体的存储逻辑由Storage接口的具体实现类去做。（如LocalStorage，QiniuStorage）</p>\n<p>具体逻辑实现可以参照这个<a href=\"https://github.com/lurenha/qiniu-demo\" target=\"_blank\" rel=\"noopener\">qiniu-demo</a></p>\n<pre class=\"language-java\"><code>\n/**\n * 提供存储服务类，所有存储服务均由该类对外提供\n */\npublic class StorageService  {\n    private String active;\n    private Storage storage;\n\n    public String getActive() {\n        return active;\n    }\n\n    public void setActive(String active) {\n        this.active = active;\n    }\n\n    public Storage getStorage() {\n        return storage;\n    }\n\n    public void setStorage(Storage storage) {\n        this.storage = storage;\n    }\n\n    /**\n     * 存储一个文件对象\n     *\n     * @param inputStream   文件输入流\n     * @param contentLength 文件长度\n     * @param contentType   文件类型\n     * @param fileName      文件索引名\n     */\n    public boolean store(InputStream inputStream, long contentLength, String contentType, String fileName) {\n        String key=generateKey(fileName);\n        storage.store(inputStream, contentLength, contentType, key);\n//        String url = generateUrl(key);\n//        业务相关\n//        保存key等信息到数据库\n        return true;\n    }\n    private String generateKey(String originalFilename) {\n        int index = originalFilename.lastIndexOf(\'.\');\n        String suffix = originalFilename.substring(index);\n        String key = UUID.randomUUID().toString();\n        key = key.replace(\"-\", \"\");\n        key+=suffix;\n        return key;\n    }\n\n    public Stream&lt;Path&gt; loadAll() {\n        return storage.loadAll();\n    }\n\n    public Path load(String keyName) {\n        return storage.load(keyName);\n    }\n\n    public Resource loadAsResource(String keyName) {\n        return storage.loadAsResource(keyName);\n    }\n\n    public void delete(String keyName) {\n        storage.delete(keyName);\n    }\n\n    private String generateUrl(String keyName) {\n        return storage.generateUrl(keyName);\n    }\n}\n</code></pre>', 'SpringBoot的两种属性值注入的方式', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 6, '2020-02-04 18:40:56', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (71, 'SpringBoot常用依赖包', '<pre class=\"language-markup\"><code> &lt;dependencies&gt;\n        &lt;!-- springboot web --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\n            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;\n        &lt;/dependency&gt;\n\n        &lt;!--aop--&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\n            &lt;artifactId&gt;spring-boot-starter-aop&lt;/artifactId&gt;\n        &lt;/dependency&gt;\n\n        &lt;!--热部署配置--&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\n            &lt;artifactId&gt;spring-boot-devtools&lt;/artifactId&gt;\n        &lt;/dependency&gt;\n\n        &lt;!-- lombok --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;\n            &lt;artifactId&gt;lombok&lt;/artifactId&gt;\n            &lt;version&gt;1.18.10&lt;/version&gt;\n        &lt;/dependency&gt;\n\n        &lt;!-- mybatis --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.mybatis.spring.boot&lt;/groupId&gt;\n            &lt;artifactId&gt;mybatis-spring-boot-starter&lt;/artifactId&gt;\n            &lt;version&gt;${mybatis.version}&lt;/version&gt;\n        &lt;/dependency&gt;\n\n        &lt;!-- 分页插件 --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;com.github.pagehelper&lt;/groupId&gt;\n            &lt;artifactId&gt;pagehelper-spring-boot-starter&lt;/artifactId&gt;\n            &lt;version&gt;${pagehelper.version}&lt;/version&gt;\n        &lt;/dependency&gt;\n\n        &lt;!-- springboot test --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\n            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;\n        &lt;/dependency&gt;\n\n        &lt;!-- 连接池 --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;com.alibaba&lt;/groupId&gt;\n            &lt;artifactId&gt;druid&lt;/artifactId&gt;\n            &lt;version&gt;${druid.version}&lt;/version&gt;\n        &lt;/dependency&gt;\n\n        &lt;!-- mysql连接包 --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;mysql&lt;/groupId&gt;\n            &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;\n            &lt;scope&gt;runtime&lt;/scope&gt;\n        &lt;/dependency&gt;\n\n        &lt;!-- json --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;net.sf.json-lib&lt;/groupId&gt;\n            &lt;artifactId&gt;json-lib&lt;/artifactId&gt;\n            &lt;version&gt;2.4&lt;/version&gt;\n            &lt;classifier&gt;jdk15&lt;/classifier&gt;\n        &lt;/dependency&gt;\n\n        &lt;!-- 工具包 --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.apache.commons&lt;/groupId&gt;\n            &lt;artifactId&gt;commons-lang3&lt;/artifactId&gt;\n        &lt;/dependency&gt;\n\n        &lt;!-- JWT token --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;com.auth0&lt;/groupId&gt;\n            &lt;artifactId&gt;java-jwt&lt;/artifactId&gt;\n            &lt;version&gt;${jwt.version}&lt;/version&gt;\n        &lt;/dependency&gt;\n\n        &lt;!-- shiro --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.apache.shiro&lt;/groupId&gt;\n            &lt;artifactId&gt;shiro-spring&lt;/artifactId&gt;\n            &lt;version&gt;${shiro.vaesion}&lt;/version&gt;\n        &lt;/dependency&gt;\n\n    &lt;/dependencies&gt;</code></pre>', 'Maven常用依赖包', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 0, 1, 1, 6, '2019-12-01 15:58:46', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (72, 'Bloom Filter(布隆过滤器)', '<h2 id=\"什么是布隆过滤器\">哈希映射存在的问题</h2>\n<p>现在有大量的数据，而这些数据的大小已经远远超出了服务器的内存，现在再给你一个数据，如何判断给你的数据在不在其中。如果服务器的内存足够大，那么用HashMap是一个不错的解决方案，理论上的时间复杂度可以达到O(1)，但是 HashMap 的实现也有缺点，例如存储容量占比高，考虑到负载因子的存在，通常空间是不能被用满的，而一旦你的值很多例如上亿的时候，那 HashMap 占据的内存大小就变得很可观了。这个时候就可以使用&ldquo;布隆过滤器&rdquo;来解决这个问题。但是还是同样的，会有一定的&ldquo;误判率&rdquo;。</p>\n<h2 id=\"什么是布隆过滤器\">什么是布隆过滤器</h2>\n<p>布隆过滤器是一个叫&ldquo;布隆&rdquo;的人提出的，它本身是一个很长的二进制向量，既然是二进制的向量，那么显而易见的，存放的不是0，就是1。</p>\n<p>所以布隆过滤器大概是是一个 bit 向量或者说 bit 数组。</p>\n<p><img src=\"https://upload-images.jianshu.io/upload_images/2785001-07e149c32a2608fa.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/600/format/webp\" alt=\"https://upload-images.jianshu.io/upload_images/2785001-07e149c32a2608fa.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/600/format/webp\" width=\"600\" height=\"151\" /></p>\n<div>\n<div>如果我们要映射一个值到布隆过滤器中，我们需要使用多个不同的哈希函数生成多个哈希值，并对每个生成的哈希值指向的 bit 位置 1，例如针对值 &ldquo;baidu&rdquo; 和三个不同的哈希函数分别生成了哈希值 1、4、7，则上图转变为：</div>\n<img src=\"https://upload-images.jianshu.io/upload_images/2785001-12449becdb038afd.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/600/format/webp\" alt=\"https://upload-images.jianshu.io/upload_images/2785001-12449becdb038afd.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/600/format/webp\" width=\"600\" height=\"290\" /><br /><br /></div>\n<div>Ok，我们现在再存一个值 &ldquo;tencent&rdquo;，如果哈希函数返回 3、4、8 的话，图继续变为：</div>\n<div><img src=\"https://upload-images.jianshu.io/upload_images/2785001-802577f6332d76b4.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/600/format/webp\" alt=\"https://upload-images.jianshu.io/upload_images/2785001-802577f6332d76b4.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/600/format/webp\" width=\"600\" height=\"310\" /></div>\n<div>\n<p>值得注意的是，4 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位，因此它被覆盖了。现在我们如果想查询 &ldquo;dianping&rdquo; 这个值是否存在，哈希函数返回了 1、5、8三个值，结果我们发现 5 这个 bit 位上的值为 0，说明没有任何一个值映射到这个 bit 位上，因此我们可以很确定地说 &ldquo;dianping&rdquo; 这个值不存在。而当我们需要查询 &ldquo;baidu&rdquo; 这个值是否存在的话，那么哈希函数必然会返回 1、4、7，然后我们检查发现这三个 bit 位上的值均为 1，那么我们可以说 &ldquo;baidu&rdquo; 存在了么？答案是不可以，只能是 &ldquo;baidu&rdquo; 这个值可能存在。</p>\n<p>这是为什么呢？答案跟简单，因为随着增加的值越来越多，被置为 1 的 bit 位也会越来越多，这样某个值 &ldquo;taobao&rdquo; 即使没有被存储过，但是万一哈希函数返回的三个 bit 位都被其他值置位了 1 ，那么程序还是会判断 &ldquo;taobao&rdquo; 这个值存在。</p>\n<h2>支持删除么</h2>\n<div>\n<div>\n<p>目前我们知道布隆过滤器可以支持 add 和 isExist 操作，那么 delete 操作可以么，答案是不可以，例如上图中的 bit 位 4 被两个值共同覆盖的话，一旦你删除其中一个值例如 &ldquo;tencent&rdquo; 而将其置位 0，那么下次判断另一个值例如 &ldquo;baidu&rdquo; 是否存在的话，会直接返回 false，而实际上你并没有删除它。</p>\n<p>如何解决这个问题，答案是计数删除。但是计数删除需要存储一个数值，而不是原先的 bit 位，会增大占用的内存大小。这样的话，增加一个值就是将对应索引槽上存储的值加一，删除则是减一，判断是否存在则是看值是否大于0。</p>\n</div>\n<h2>如何选择哈希函数个数和布隆过滤器长度</h2>\n<div>\n<div>\n<p>很显然，过小的布隆过滤器很快所有的 bit 位均为 1，那么查询任何值都会返回&ldquo;可能存在&rdquo;，起不到过滤的目的了。布隆过滤器的长度会直接影响误报率，布隆过滤器越长其误报率越小。</p>\n<p>另外，哈希函数的个数也需要权衡，个数越多则布隆过滤器 bit 位置位 1 的速度越快，且布隆过滤器的效率越低；但是如果太少的话，那我们的误报率会变高。</p>\n</div>\n<img src=\"https://upload-images.jianshu.io/upload_images/2785001-76dccfbdc9d7bdb1.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/600/format/webp\" alt=\"https://upload-images.jianshu.io/upload_images/2785001-76dccfbdc9d7bdb1.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/600/format/webp\" width=\"600\" height=\"342\" /></div>\n<div>k 为哈希函数个数，m 为布隆过滤器长度，n 为插入的元素个数，p 为误报率。<br />至于如何推导这个公式，我在知乎发布的<a href=\"https://zhuanlan.zhihu.com/p/43263751\" target=\"_blank\" rel=\"nofollow noopener\">文章</a>有涉及，感兴趣可以看看，不感兴趣的话记住上面这个公式就行了。</div>\n<div>\n<h2>使用场景</h2>\n<ol>\n<li>判断给定数据是否存在：比如判断一个数字是否在于包含大量数字的数字集中（数字集很大，5亿以上！）、 防止缓存穿透（判断请求的数据是否有效避免直接绕过缓存请求数据库）等等、邮箱的垃圾邮件过滤、黑名单功能等等。</li>\n<li>去重：比如爬给定网址的时候对已经爬取过的 URL 去重。</li>\n</ol>\n<h3 id=\"guava实现布隆过滤器\">guava实现布隆过滤器</h3>\n<p>现在相信你对布隆过滤器应该有一个比较感性的认识了，布隆过滤器核心思想其实并不难，难的在于如何设计随机映射函数，到底映射几次，二进制向量的长度设置为多少比较好，这可能就不是一般的开发可以驾驭的了，好在Google大佬给我们提供了开箱即用的组件，来帮助我们实现布隆过滤器，现在就让我们看看怎么Google大佬送给我们的&ldquo;礼物&rdquo;吧。</p>\n<pre class=\"language-markup\"><code> &lt;dependency&gt;\n      &lt;groupId&gt;com.google.guava&lt;/groupId&gt;\n      &lt;artifactId&gt;guava&lt;/artifactId&gt;\n      &lt;version&gt;19.0&lt;/version&gt;\n &lt;/dependency&gt;</code></pre>\n<pre class=\"language-java\"><code> private static int size = 1000000;//预计要插入多少数据\n\n    private static double fpp = 0.01;//期望的误判率\n\n    private static BloomFilter&lt;Integer&gt; bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, fpp);\n\n    public static void main(String[] args) {\n        //插入数据\n        for (int i = 0; i &lt; 1000000; i++) {\n            bloomFilter.put(i);\n        }\n        int count = 0;\n        for (int i = 1000000; i &lt; 2000000; i++) {\n            if (bloomFilter.mightContain(i)) {\n                count++;\n                System.out.println(i + \"误判了\");\n            }\n        }\n        System.out.println(\"总共的误判数:\" + count);\n    }</code></pre>\n<p>代码简单分析：<br />我们定义了一个布隆过滤器，有两个重要的参数，分别是 我们预计要插入多少数据，我们所期望的误判率，误判率不能为0。<br />我向布隆过滤器插入了0-1000000，然后用1000000-2000000来测试误判率。</p>\n<p>运行结果：</p>\n<pre class=\"language-java\"><code>1999501误判了\n1999567误判了\n1999640误判了\n1999697误判了\n1999827误判了\n1999942误判了\n总共的误判数:10314</code></pre>\n<p>现在总共有100万数据是不存在的，误判了10314次，我们计算下误判率，和我们定义的期望误判率0.01相差无几。</p>\n</div>\n<div><hr /><a href=\"https://www.jianshu.com/p/2104d11ee0a2\" target=\"_blank\" rel=\"noopener\">参考资料1</a></div>\n</div>\n</div>\n<p><a href=\"https://www.cnblogs.com/xujian2014/p/5491286.html\" target=\"_blank\" rel=\"noopener\">参考资料2</a></p>', '布隆过滤器', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 6, '2020-02-15 18:23:25', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (73, 'BitSet', '<h2>什么是BitSet？</h2>\n<p>　BitSet类实现了一个按需增长的位向量。位Set的每一个组件都有一个boolean值。用非负的整数将BitSet的位编入索引。可以对每个编入索引的位进行测试、设置或者清除。通过逻辑与、逻辑或和逻辑异或操作，可以使用一个 BitSet修改另一个&nbsp;BitSet的内容。&nbsp;</p>\n<p>　　默认情况下，set 中所有位的初始值都是false。&nbsp;</p>\n<p>　　每个位 set 都有一个当前大小，也就是该位 set 当前所用空间的位数。注意，这个大小与位 set 的实现有关，所以它可能随实现的不同而更改。位 set 的长度与位 set 的逻辑长度有关，并且是与实现无关而定义的。&nbsp;</p>\n<h2><span class=\"number\"><span class=\"keyword\">使用场景</span></span></h2>\n<p>常见的应用场景是对海量数据进行一些统计工作，比如日志分析、用户数统计等。</p>\n<p>　　阿里的面试问过一道题：有1千万个随机数，随机数的范围在1到1亿之间。现在要求写出一种算法，将1到1亿之间没有在随机数中的数求出来？</p>\n<p>　　代码示例如下：　</p>\n<pre class=\"language-java\"><code>public class Alibaba\n{\n    public static void main(String[] args)\n    {\n        Random random=new Random();\n        \n        List&lt;Integer&gt; list=new ArrayList&lt;&gt;();\n        for(int i=0;i&lt;10000000;i++)\n        {\n            int randomResult=random.nextInt(100000000);\n            list.add(randomResult);\n        }\n        System.out.println(\"产生的随机数有\");\n        for(int i=0;i&lt;list.size();i++)\n        {\n            System.out.println(list.get(i));\n        }\n        BitSet bitSet=new BitSet(100000000);\n        for(int i=0;i&lt;10000000;i++)\n        {\n            bitSet.set(list.get(i));\n        }\n        \n        System.out.println(\"0~1亿不在上述随机数中有\"+bitSet.size());\n        for (int i = 0; i &lt; 100000000; i++)\n        {\n            if(!bitSet.get(i))\n            {\n                System.out.println(i);\n            }\n        }     \n    }\n}</code></pre>\n<h2>原理</h2>\n<p>Java的BitSet使用一个Long（一共64位）的数组中的每一位（bit）是否为<code>1</code>来表示当前Index的数是否存在。但是BitSet又是如何实现的呢？其实只需要理解其中的两个方法：</p>\n<ul>\n<li>set</li>\n<li>get</li>\n</ul>\n<h2>set</h2>\n<p>先看源代码：</p>\n<pre class=\"language-java\"><code>public void set(int bitIndex) {\n    if (bitIndex &lt; 0)\n        throw new IndexOutOfBoundsException(\"bitIndex &lt; 0: \" + bitIndex);\n\n    int wordIndex = wordIndex(bitIndex);\n    expandTo(wordIndex);\n\n    words[wordIndex] |= (1L &lt;&lt; bitIndex); // Restores invariants\n\n    checkInvariants();\n}</code></pre>\n<p>除了判断给的值是否小于0的那两句，我们依次来看一下这个函数的每一句代码。</p>\n<h3>wordIndex</h3>\n<p>第一句就是计算wordIndex，通过<code>wordIndex</code>函数获取值。代码如下：</p>\n<pre class=\"language-java\"><code>private static int wordIndex(int bitIndex) {\n    return bitIndex &gt;&gt; 6;\n}</code></pre>\n<p><strong>为什么是6呢？而不是其他值呢？</strong></p>\n<div>\n<div>因为在Java里Long类型是64位，要计算给定的参数<code>bitIndex</code>应该放在数组（在BitSet里存在<code>word</code>的实例变量里）的哪个long里，只需要计算：<code>bitIndex / 64</code>即可，这里使用<code>&gt;&gt;</code>来代替除法（因为位运算要比除法效率高）。而64正好是2的6次幂。</div>\n<div>通过<code>wordIndex</code>函数就能计算出参数<code>bitIndex</code>应该存放在<code>words</code>数组里的哪一个long里。</div>\n<div>\n<h3>expandTo</h3>\n<pre class=\"language-java\"><code>private void expandTo(int wordIndex) {\n    int wordsRequired = wordIndex+1;\n    if (wordsInUse &lt; wordsRequired) {\n        ensureCapacity(wordsRequired);\n        wordsInUse = wordsRequired;\n    }\n}</code></pre>\n<div>\n<div>从上面已经知道在BitSet里是通过一个Long数组（<code>words</code>）来存放数据的，这里的<code>expandTo</code>方法就是用来判断<code>words</code>数组的长度是否大于当前所计算出来的<code>wordIndex</code>，如果超过当前<code>words</code>数组的长度，也即是存不下，则新加一个long数到<code>words</code>里(ensureCapacity(wordsRequired)所实现的)。</div>\n<div>\n<h3>Restores invariants</h3>\n<pre class=\"language-java\"><code>words[wordIndex] |= (1L &lt;&lt; bitIndex); // Restores invariants</code></pre>\n</div>\n</div>\n</div>\n<pre class=\"language-java\"><code>System.out.println(Integer.toBinaryString(1&lt;&lt;0));\nSystem.out.println(Integer.toBinaryString(1&lt;&lt;1));\nSystem.out.println(Integer.toBinaryString(1&lt;&lt;2));\nSystem.out.println(Integer.toBinaryString(1&lt;&lt;3));\nSystem.out.println(Integer.toBinaryString(1&lt;&lt;4));\nSystem.out.println(Integer.toBinaryString(1&lt;&lt;5));\nSystem.out.println(Integer.toBinaryString(1&lt;&lt;6));\nSystem.out.println(Integer.toBinaryString(1&lt;&lt;7));​</code></pre>\n<p>输出是：</p>\n<pre class=\"language-java\"><code>1\n10\n100\n1000\n10000\n100000\n1000000\n10000000</code></pre>\n<div>\n<div>上面所有的输出中 <code>1</code> 所在的位置，正好是第1，2，3，4，5，6，7，8（Java数组的Index从0开始）位。BitSet正是通过这种方式，将所给的<code>bitIndex</code>对应位设置成1，表示这个数已经存在了。（因为BitSet是使用的Long，所以要使用1L来进行位移）（当bitIndex大于63时会轮回 如1&lt;&lt;0等于1&lt;&lt;64等于1&lt;&lt;128）。</div>\n<div>搞懂了<code>(1L &lt;&lt; bitIndex)</code>，剩下的就是用<code>|</code>来将当前算出来的和以前的值进行合并了<code>words[wordIndex] |= (1L &lt;&lt; bitIndex);</code>。</div>\n<div>\n<h2>get</h2>\n<p>搞懂了<code>set</code>方法，那么<code>get</code>方法也就好懂了，整体意思就是算出来所给定的<code>bitIndex</code>所对应的位数是否为<code>1</code>即可。先看看代码：</p>\n<pre class=\"language-java\"><code>public boolean get(int bitIndex) {\n    if (bitIndex &lt; 0)\n        throw new IndexOutOfBoundsException(\"bitIndex &lt; 0: \" + bitIndex);\n\n    checkInvariants();\n\n    int wordIndex = wordIndex(bitIndex);\n    return (wordIndex &lt; wordsInUse)\n        &amp;&amp; ((words[wordIndex] &amp; (1L &lt;&lt; bitIndex)) != 0);\n}</code></pre>\n<div>\n<div>计算<code>wordIndex</code>在上面set方法里已经说明了，就不再细述。这个方法里，最重要的就只有：<code>words[wordIndex] &amp; (1L &lt;&lt; bitIndex)</code>。这里<code>(1L &lt;&lt; bitIndex)</code>也已经做过说明，就是算出一个数，只有<code>bitIndex</code>位上为1，其他都为0，然后再和<code>words[wordIndex]</code>做<code>&amp;</code>计算，如果<code>words[wordIndex]</code>数的<code>bitIndex</code>位是<code>0</code>，则结果就是<code>0</code>，以此来判断参数<code>bitIndex</code>存在不。</div>\n<br /><hr /></div>\n</div>\n<a href=\"https://www.jianshu.com/p/4fbad3a6d253\" target=\"_blank\" rel=\"noopener\">点击查看原文</a><br /><br /></div>\n</div>', ' BitSet使用场景和原理', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 6, '2020-02-15 21:06:49', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (74, '面试题04. 二维数组中的查找', '<p>最近leetcode出了剑指offer的题，我们来看下这个题</p>\n<div><code>在一个 n * m 的二维数组中，每一行都按照从左到右递增的顺序排序，每一列都按照从上到下递增的顺序排序。请完成一个函数，输入这样的一个二维数组和一个整数，判断数组中是否含有该整数。</code></div>\n<div>&nbsp;</div>\n<div><code>示例:</code></div>\n<div>&nbsp;</div>\n<div><code>现有矩阵 matrix 如下：</code></div>\n<div>&nbsp;</div>\n<div>\n<div>[</div>\n<div>&nbsp; [1,&nbsp; &nbsp;4,&nbsp; 7, 11, 15],</div>\n<div>&nbsp; [2,&nbsp; &nbsp;5,&nbsp; 8, 12, 19],</div>\n<div>&nbsp; [3,&nbsp; &nbsp;6,&nbsp; 9, 16, 22],</div>\n<div>&nbsp; [10, 13, 14, 17, 24],</div>\n<div>&nbsp; [18, 21, 23, 26, 30]</div>\n<div>]</div>\n<div>&nbsp;</div>\n</div>\n<div><code>给定 target = 5，返回 true。</code></div>\n<div>&nbsp;</div>\n<div><code>给定 target = 20，返回 false。</code></div>\n<div>&nbsp;</div>\n<div>&nbsp;</div>\n<div><code>限制：</code></div>\n<div>&nbsp;</div>\n<div><code>0 &lt;= n &lt;= 1000</code></div>\n<div>&nbsp;</div>\n<div><code>0 &lt;= m &lt;= 1000</code></div>\n<hr />\n<h2>分析</h2>\n<p>这题是从给定二维数组里面找到目标值看是否存在，最暴力的方法自然是遍历一遍。然而人家给的数组是有规律的,我们研究下：</p>\n<p><code>每一行都按照从左到右递增的顺序排序，每一列都按照从上到下递增的顺序排序。</code></p>\n<p><code></code><code></code></p>\n<p><code></code></p>\n<p><code></code><code></code></p>\n<p>所以数组左上角，右下角分别是最小，最大的值。</p>\n<p>再看左下角，右上角。</p>\n<p>这两个点是对称的。我们拿左下角举例：<code>如果我们要找的数字大于左下角那么可以直接排除[1，2，3，10，18]一列，如果数字小于左下角排除[18, 21, 23, 26, 30]一行</code>，感觉效率提升飞起啊。</p>\n<p>我们可以从左下角开始设变量x，<code>target&lt;x则排除 x所在行（x是这一行最小的）接着去x上边寻找，target&gt;x则排除x所在列（x是这一列中最大的）接着去x右边寻找。</code></p>\n<p>右上角同理。</p>\n<p>代码选左下角开始寻找：</p>\n<pre class=\"language-java\"><code> public boolean findNumberIn2DArray(int[][] matrix, int target) {\n        if(matrix.length&lt;=0||matrix[0].length&lt;=0)\n            return false;\n\n        int xmax = matrix.length - 1;\n        int ymax = matrix[0].length - 1;\n        if (target &gt; matrix[xmax][ymax] || target &lt; matrix[0][0]) {\n            return false;\n        }\n\n        int i = xmax;\n        for (int j = 0; j &lt;= ymax; j++) {\n            if (target &lt; matrix[i][j]) {//target&gt;x时,x向上移动\n                if(i==0)\n                    return false;\n                i--;\n                j--;\n            }else if(target == matrix[i][j]){\n                return true;\n            }else{ //target&lt;x时,j++,x向右移动\n                continue;\n            }\n           \n        }\n\n        return false;\n    }</code></pre>\n<p><img class=\"wscnph\" src=\"\" /></p>', '在一个 n * m 的二维数组中，每一行都按照从左到右递增的顺序排序，每一列都按照从上到下递增的顺序排序。请完成一个函数，输入这样的一个二维数组和一个整数，判断数组中是否含有该整数。', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 2, '2020-02-27 10:49:02', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (75, '面试题14- I. 剪绳子', '<p>给你一根长度为 n 的绳子，请把绳子剪成整数长度的 m 段（m、n都是整数，n&gt;1并且m&gt;1），每段绳子的长度记为 k[0],k[1]...k[m] 。请问 k[0]*k[1]*...*k[m] 可能的最大乘积是多少？</p>\n<p>例如，当绳子的长度是8时，我们把它剪成长度分别为2、3、3的三段，此时得到的最大乘积是18。</p>\n<pre class=\"language-markup\"><code>示例 1：\n\n输入: 2\n输出: 1\n解释: 2 = 1 + 1, 1 &times; 1 = 1</code></pre>\n<pre class=\"language-markup\"><code>示例 2:\n\n输入: 10\n输出: 36\n解释: 10 = 3 + 3 + 4, 3 &times; 3 &times; 4 = 36</code></pre>\n<hr />\n<h2>分析：一.动态规划</h2>\n<p>设&nbsp;<span class=\"katex\"><span class=\"katex-mathml\">F(n) </span></span>为长度为&nbsp;<span class=\"katex\"><span class=\"katex-mathml\">n</span></span>&nbsp;的绳子可以得到的最大乘积，对于每一个&nbsp;<span class=\"katex\"><span class=\"katex-mathml\">F(n)</span></span>，可以得到如下分解：</p>\n<p><img src=\"https://pic.leetcode-cn.com/86e7c3368e2edd8c4bfc907b322204198b56cce7e82e7da9a43a8bbaab50cf9e-14.png\" alt=\"\" width=\"600\" /></p>\n<p>注：我们在1*F（n-1），2*F（n-2）... 这层可以不继续分解，把1*（n-1），（2*n-2）....当作情况之一（我们不必一定到达叶子节点，可以在中间任意一层停止）</p>\n<p>所以状态转移方程dp[i]=Max(1*dp（i-1）,1*（i-1）,2*dp（i-2),（2*i-2）......)</p>\n<pre class=\"language-java\"><code>class Solution {\n  \n    public int cuttingRope(int n) {\n        int[] dp = new int[n + 1];\n        for (int i = 2; i &lt; dp.length; i++) {\n            for (int j = 1; j &lt; i/2+1; j++) {//i/2+1剪枝\n                dp[i]=max3(j*(i-j),j*dp[i-j],dp[i]);\n            }\n        }\n        return dp[n];\n    }\n\n    private int max3(int a, int b, int c) {\n        return Math.max(Math.max(a, b),c);\n    }\n}</code></pre>\n<p><img class=\"wscnph\" src=\"\" /></p>\n<hr />\n<h2>二.贪心法</h2>\n<p>借鉴大佬的方法:<a href=\"https://leetcode-cn.com/problems/jian-sheng-zi-lcof/solution/mian-shi-ti-14-i-jian-sheng-zi-tan-xin-si-xiang-by/\" target=\"_blank\" rel=\"noopener\">点击查看</a></p>\n<p><img src=\"http://sjpeng.top/QQ%E6%88%AA%E5%9B%BE20200229155807.png\" alt=\"\" /></p>\n<p><img src=\"http://sjpeng.top/QQ%E6%88%AA%E5%9B%BE20200229155824.png\" alt=\"\" /></p>\n<pre class=\"language-java\"><code>class Solution {\n      public int cuttingRope(int n) {\n        \n        if(n&lt;=3) return n - 1;\n        int a=n/3;\n        int b = n % 3;\n        if(b==0) return (int) Math.pow(3, a);\n        if(b==1) return (int) Math.pow(3, a - 1) * 4;\n        return (int) Math.pow(3, a) * 2;\n    }\n}</code></pre>', '给你一根长度为 n 的绳子，请把绳子剪成整数长度的 m 段（m、n都是整数，n>1并且m>1），每段绳子的长度记为 k[0],k[1]...k[m] 。请问 k[0]*k[1]*...*k[m] 可能的最大乘积是多少？', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 2, '2020-02-29 15:56:49', '2020-04-01 11:41:51');
INSERT INTO `t_blog` VALUES (76, '使用AOP实现一个MyCache注解', '<h2>问题：不改变业务代码的基础上进行增强</h2>\n<p>最近在学习Redis，在Web项目中通过手动判断Redis中是否有对应的key去使用缓存，大概是这样：</p>\n<pre class=\"language-java\"><code>    public List&lt;Student&gt; findStudent() {\n        String key=\"findStudent\";\n        //缓存中有：返回缓存内容\n        if(redisUtil.hasKey(key)){\n            return (List&lt;Student&gt;) redisUtil.get(key);\n        }\n       \n        //缓存中没有：去数据库查询，再存到缓存\n        List&lt;Student&gt; studentList = studentDao.findAll();\n        redisUtil.set(key,studentList);\n        return studentList ;\n    }</code></pre>\n<p>上一个没有缓存前的代码对比下：</p>\n<pre class=\"language-java\"><code>    public List&lt;Student&gt; findStudent() {\n        List&lt;Student&gt; studentList = studentDao.findAll();\n        return studentList ;\n    }</code></pre>\n<p>感觉还是有些麻烦的，我开始考虑有没有办法不改变业务代码并实现缓存的功能。。。果然，天无绝人之路，Spring的AOP完美解决了这个问题。</p>\n<hr />\n<h2>AOP介绍</h2>\n<p>本部分内容出自：<a href=\"https://www.jianshu.com/p/c6d3799f3ab3\" target=\"_blank\" rel=\"noopener\">点击查看</a></p>\n<div>\n<div>\n<p>OP概念：面向切面编程：扩展功能不修改源代码实现</p>\n<p>AOP采用横向抽取机制，取代传统的纵向继承体系重复性代码（性能监视，事务管理，安全检查，缓存）</p>\n<p>（动态代理实现）</p>\n<hr />\n<p>AOP操作术语：</p>\n<p>（JoinPoint）连接点：类里面可以被增强的方法</p>\n<p>（Pointcut）切入点：实际增强的方法，也就是新增的方法</p>\n<p>（Advice）通知/增强：增强的逻辑，比如扩展日志功能，这个日志功能增强</p>\n<p>&nbsp; 前置通知、后置通知、异常通知、最终通知、环绕通知</p>\n<p>（Aspect）切面：把增强应用到具体方法上，过程称为切面</p>\n</div>\n<hr />\n<div>\n<div>\n<p>AOP操作准备</p>\n<p>1.导入AOP相关jar包</p>\n<p>aopalliance-1.0.jar</p>\n<p>aspectjweaver-1.8.7.jar</p>\n<p>spring-aop.RELEASE.jar</p>\n<p>spring-aspects-RELEASE.jar</p>\n<p>2.创建spring核心配置文件，导入aop约束spring-aop.xsd</p>\n<p>3.使用表达式配置切入点</p>\n<p>（1）切入点：实际增强的方法</p>\n<p>（2）常用的表达式</p>\n<p>execution(<strong>方法修饰符 方法返回值 方法所属类 匹配方法名(方法中的形参表)&nbsp; 方法申明抛出的异常</strong>)</p>\n<p>`execution(*空格com.Book.add(..))&nbsp;&nbsp; add()方法</p>\n<p>`execution(*空格com.Book.*(..))&nbsp;&nbsp;&nbsp; 类中的所有方法</p>\n<p>`execution(*空格*.*(..))&nbsp;&nbsp;&nbsp;&nbsp; 所有类中的所有方法</p>\n<p>`execution(*空格save*(..))&nbsp;&nbsp; 匹配所有以save开头的方法名</p>\n<p>&lt;!--配置文件中--&gt;</p>\n<p>&lt;bean id=\"\" class=\"\"&gt;</p>\n<p>&lt;!--配置aop操作--&gt;</p>\n<p>&lt;aop:config&gt;</p>\n<p>&lt;!--配置切入点--&gt;</p>\n<p>&lt;aop:pointcut expression=\"execution(*空格com.aop.Book.*(..)) id=\"pointcut1（切入点名字）\"&gt;</p>\n<p>&lt;!--配置切面&nbsp; 把增强用到方法上--&gt;</p>\n<p>&lt;aop:aspect ref=\"增强类对象的id\" &gt;</p>\n<p>&lt;!--配置增强类型 method:增强类使用在哪个方法上作为前置--&gt;</p>\n<p>&lt;aop:before method=\"before1\" pointcut-ref=\"pointcut1\"/&gt;</p>\n<p>&lt;!--后置通知--&gt;</p>\n<p>&lt;aop:after-returning method=\"after1\" pointcut-ref=\"pointcut1\"/&gt;</p>\n<p>&lt;!--环绕通知--&gt;</p>\n<p>&lt;aop:around method=\"around1\" pointcut-ref=\"pointcut1\"/&gt;</p>\n<p>&lt;/aop:aspect&gt;</p>\n<p>&lt;/aop:config&gt;</p>\n<p>测试：环绕通知</p>\n<p>public void around(ProceedingJoinPoint proceedingJoinPoint){</p>\n<p>//方法之前代码</p>\n<p>//执行要被增强的方法</p>\n<p>proceedingJoinPoint.proceed();</p>\n<p>//方法之后代码</p>\n<p>}</p>\n<hr />\n<h2>开始&nbsp;</h2>\n<p>接下来开始重头戏，自己实现一个MyCache注解。</p>\n<p>步骤如下：1.定义一个注解 2.将注解添加到需要缓存的方法上 3.利用AOP的环绕通知对方法增强</p>\n<p>1.声明一个注解</p>\n<pre class=\"language-java\"><code>@Target(ElementType.METHOD)//注解作用在方法上\n@Retention(RetentionPolicy.RUNTIME)//注解不仅被保存到class文件中，jvm加载class文件之后，仍然存在\npublic @interface MyCache {\n}\n</code></pre>\n</div>\n</div>\n2.在需要增强的方法上加上我们的注解</div>\n<div>\n<pre class=\"language-java\"><code>    @MyCache\n    @Override\n    public List&lt;Tag&gt; findAllPro() {\n        List&lt;Tag&gt; tagList = tagDao.findallTagPro();\n        return tagList;\n    }</code></pre>\n</div>\n<p>3.利用AOP的环绕通知对方法增强</p>\n<pre class=\"language-java\"><code>@Slf4j\n@Aspect\n@Component\npublic class MyCacheAspect {\n    @Autowired\n    private RedisUtil redisUtil;\n\n    //匹配com.peng.service.Impl包下所有带MyCache注解的方法\n    @Around(\"execution(public * com.peng.service.Impl..*(..)) &amp;&amp; @annotation(myCache)\")\n    public Object around(ProceedingJoinPoint jp, MyCache myCache) throws Throwable {\n        long startTime = System.currentTimeMillis();\n        //生成对应Redis的key（生成规则可以自行定义）\n        Signature signature = jp.getSignature();\n        String methodName = signature.getName();\n        String className = signature.getDeclaringTypeName();\n        StringBuffer sbKey = new StringBuffer();\n        sbKey.append(className);\n        sbKey.append(\".\");\n        sbKey.append(methodName);\n        Object[] args = jp.getArgs();//方法参数值\n        for (Object object:args) {\n            sbKey.append(\"-\");\n            sbKey.append(object);\n        }\n        String key=sbKey.toString();\n        //如果有缓存直接返回，没有正常执行并写入缓存\n        try {\n            if (redisUtil.hasKey(key)) {\n                System.out.println(methodName+\"-\"+\"找到缓存了！\");\n                return redisUtil.get(key);\n            } else {\n                System.out.println(methodName+\"----------------没有缓存！\");\n                Object result = jp.proceed(args);\n                redisUtil.set(key, result, 60 * 60);\n                return result;\n            }\n        } catch (Throwable t){\n            log.error(t.toString());\n            return null;\n        } finally {\n            System.out.print(methodName+\"-\"+\"方法执行时间：\");\n            System.out.println(System.currentTimeMillis()-startTime);\n        }\n    }\n}</code></pre>\n<p>执行结果：执行三次 第一次没有缓存 后两次命中缓存</p>\n<pre class=\"language-markup\"><code>findAllPro----------------没有缓存！\nfindAllPro-方法执行时间：506\nfindAllPro-找到缓存了！\nfindAllPro-方法执行时间：5\nfindAllPro-找到缓存了！\nfindAllPro-方法执行时间：3</code></pre>', '1.定义一个注解 2.将注解添加到需要缓存的方法上 3.利用AOP的环绕通知对方法增强', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1585061086950&di=50b428cca8f28dc02384cb26620af99d&imgtype=0&src=http%3A%2F%2Fimage.biaobaiju.com%2Fuploads%2F20190705%2F21%2F1562334666-yVOlRJCDGs.jpg', 0, 1, 1, 1, 6, '2020-03-21 17:59:40', '2020-04-01 11:41:51');

-- ----------------------------
-- Table structure for t_blog_tag
-- ----------------------------
DROP TABLE IF EXISTS `t_blog_tag`;
CREATE TABLE `t_blog_tag`  (
  `bl_id` int(11) NOT NULL,
  `ta_id` int(11) NOT NULL,
  INDEX `bl_id`(`bl_id`) USING BTREE,
  INDEX `ta_id`(`ta_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_blog_tag
-- ----------------------------
INSERT INTO `t_blog_tag` VALUES (45, 1);
INSERT INTO `t_blog_tag` VALUES (46, 1);
INSERT INTO `t_blog_tag` VALUES (54, 4);
INSERT INTO `t_blog_tag` VALUES (60, 5);
INSERT INTO `t_blog_tag` VALUES (61, 6);
INSERT INTO `t_blog_tag` VALUES (62, 3);
INSERT INTO `t_blog_tag` VALUES (63, 5);
INSERT INTO `t_blog_tag` VALUES (65, 4);
INSERT INTO `t_blog_tag` VALUES (68, 4);
INSERT INTO `t_blog_tag` VALUES (47, 2);
INSERT INTO `t_blog_tag` VALUES (47, 3);
INSERT INTO `t_blog_tag` VALUES (69, 3);
INSERT INTO `t_blog_tag` VALUES (69, 2);
INSERT INTO `t_blog_tag` VALUES (70, 7);
INSERT INTO `t_blog_tag` VALUES (71, 7);
INSERT INTO `t_blog_tag` VALUES (66, 4);
INSERT INTO `t_blog_tag` VALUES (67, 4);
INSERT INTO `t_blog_tag` VALUES (64, 4);
INSERT INTO `t_blog_tag` VALUES (75, 4);
INSERT INTO `t_blog_tag` VALUES (76, 7);

-- ----------------------------
-- Table structure for t_comment
-- ----------------------------
DROP TABLE IF EXISTS `t_comment`;
CREATE TABLE `t_comment`  (
  `co_id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '评论者姓名',
  `email` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '邮箱',
  `content` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '内容',
  `is_admin` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否是博主',
  `is_delete` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否逻辑删除',
  `bl_id` int(11) NOT NULL COMMENT '博客id',
  `parent_id` int(11) NULL DEFAULT NULL COMMENT '父节点',
  `ip_address` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'IP地址',
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  PRIMARY KEY (`co_id`) USING BTREE,
  INDEX `bl_id`(`bl_id`) USING BTREE,
  INDEX `comment_ibfk_2`(`parent_id`) USING BTREE,
  INDEX `comment_ibfk_3`(`ip_address`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 57 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_comment
-- ----------------------------
INSERT INTO `t_comment` VALUES (1, '游客one', '1333333@qq.com', '文章不错点赞！', 1, 0, 1, NULL, '1', '2019-06-23 13:09:35', '2020-03-31 20:11:29');
INSERT INTO `t_comment` VALUES (2, '游客two', '456798@qqcom', '随便说点什么吧！！！', 0, 0, 1, 1, '1', '2019-06-23 13:10:09', '2020-03-31 20:11:26');
INSERT INTO `t_comment` VALUES (3, '雷浩', '123456@qq.com', '内容内容内容内容内容内容', 0, 0, 1, NULL, '1', '2019-06-24 00:00:00', '2020-03-31 20:11:24');
INSERT INTO `t_comment` VALUES (4, '测试', '123@qq.com', 'ceshi', 0, 0, 1, 1, '1', '2019-11-16 05:51:01', '2020-03-31 20:11:28');
INSERT INTO `t_comment` VALUES (5, '测试二', '111@qq.com', '哈哈', 0, 0, 1, 1, '1', '2019-11-16 05:51:01', '2020-03-31 20:11:28');
INSERT INTO `t_comment` VALUES (6, '王五', '555@qq.com', '回复雷浩', 0, 0, 1, 3, '1', '2019-11-09 12:25:05', '2020-03-31 20:11:30');
INSERT INTO `t_comment` VALUES (7, '阿斯', '2910819096@qq.com', '66666', 0, 0, 1, 3, '1', '2019-11-09 12:28:51', '2020-03-31 20:11:22');
INSERT INTO `t_comment` VALUES (8, '77', '775736156@qq.com', '777', 0, 0, 1, 3, '1', '2019-11-09 12:30:43', '2020-03-31 20:11:22');
INSERT INTO `t_comment` VALUES (9, '888', '365651975@qq.com', '回复77', 0, 0, 1, 8, '1', '2019-11-09 12:31:57', '2020-03-31 20:11:23');
INSERT INTO `t_comment` VALUES (12, '阿飞', '775736156@qq.com', '啊哈哈', 0, 0, 1, NULL, '1', '2019-11-10 01:12:44', '2020-03-31 20:11:25');
INSERT INTO `t_comment` VALUES (13, '爱迪生', '365651975@qq.com', '6', 0, 0, 1, 12, '1', '2019-11-10 01:13:42', '2020-03-31 20:11:24');
INSERT INTO `t_comment` VALUES (14, '阿斯达', '365651975@qq.com', '你好爱迪生', 0, 0, 1, 13, '1', '2019-11-10 01:13:56', '2020-03-31 20:11:24');
INSERT INTO `t_comment` VALUES (15, '张无忌', '775736156@qq.com', '比较明显的评论！！！！！！！！！！！！！！', 0, 0, 1, 2, '1', '2019-11-12 09:04:52', '2020-03-31 20:11:27');
INSERT INTO `t_comment` VALUES (16, '赵敏', '2910819096@qq.com', '我是赵敏啊！', 0, 0, 1, 15, '1', '2019-11-12 09:08:55', '2020-03-31 20:11:27');
INSERT INTO `t_comment` VALUES (17, '张无忌', '1143038749@qq.com', '优秀啊 优秀', 0, 0, 1, 16, '1', '2019-11-12 09:12:40', '2020-03-31 20:11:27');
INSERT INTO `t_comment` VALUES (18, '路人哈哈', '775736156@qq.com', '你们都这么厉害吗', 0, 0, 1, NULL, '1', '2019-11-12 11:58:23', '2020-03-31 20:11:04');
INSERT INTO `t_comment` VALUES (25, 'nickname', '123@qq.com', '测试的', 0, 0, 1, NULL, '1', '2019-11-18 09:48:27', '2020-03-31 20:11:04');
INSERT INTO `t_comment` VALUES (26, '555', '775736156@qq.com', '12312321312312', 0, 0, 1, NULL, '1', '2019-11-18 17:06:40', '2020-03-31 20:11:03');
INSERT INTO `t_comment` VALUES (27, '555', '775736156@qq.com', '55555555555555555', 0, 0, 1, NULL, '1', '2019-11-18 17:07:57', '2020-03-31 20:11:03');
INSERT INTO `t_comment` VALUES (28, '555', '775736156@qq.com', '321', 0, 0, 1, NULL, '1', '2019-11-18 17:08:27', '2020-03-31 20:11:02');
INSERT INTO `t_comment` VALUES (33, '测试人员', '666@qq.com', '随便说点', 0, 0, 1, NULL, '2', '2019-11-22 18:48:30', '2020-03-31 20:11:02');
INSERT INTO `t_comment` VALUES (34, '测试人员', '666@qq.com', '谁啊', 0, 0, 12, NULL, '2', '2019-11-22 18:49:33', '2020-03-21 22:27:06');
INSERT INTO `t_comment` VALUES (35, '测试人员', '666@qq.com', '厉害', 0, 0, 12, 34, '2', '2019-11-22 18:49:41', '2020-03-21 22:27:06');
INSERT INTO `t_comment` VALUES (36, '测试人员', '666@qq.com', '努伊斯', 0, 0, 12, 35, '2', '2019-11-22 18:49:48', '2020-03-21 22:27:06');
INSERT INTO `t_comment` VALUES (37, '哈哈', '111@qq.com', '来来来', 0, 0, 1, NULL, '3', '2019-11-24 10:13:33', '2020-03-31 20:11:01');
INSERT INTO `t_comment` VALUES (38, '哈哈', '111@qq.com', '66666', 0, 0, 4, NULL, '3', '2019-11-24 10:14:01', '2020-03-21 22:27:06');
INSERT INTO `t_comment` VALUES (39, '张三', '775736156@qq.com', '牛皮', 0, 0, 4, NULL, '4', '2019-11-26 20:26:14', '2020-03-21 22:27:06');
INSERT INTO `t_comment` VALUES (40, '游客', '114@qq.com', '啊哈', 0, 0, 59, NULL, '5', '2019-12-25 11:14:30', '2020-03-21 22:27:06');
INSERT INTO `t_comment` VALUES (41, '张三', '114@qq.com', '嘞好啊ớ ₃ờ', 0, 0, 59, 40, '5', '2019-12-25 11:14:58', '2020-03-21 22:27:06');
INSERT INTO `t_comment` VALUES (42, '李四', '114@qq.com', '哈哈哈', 0, 0, 59, 41, '5', '2019-12-25 11:15:24', '2020-03-21 22:27:06');
INSERT INTO `t_comment` VALUES (43, '测试人员', '666@qq.com', '嘤嘤嘤', 0, 0, 1, 2, '6', '2020-01-08 16:51:48', '2020-03-31 20:11:29');
INSERT INTO `t_comment` VALUES (44, '张三', '111@qq.com', '哈哈哈', 0, 0, 68, NULL, '7', '2020-01-20 22:38:47', '2020-03-21 22:27:06');
INSERT INTO `t_comment` VALUES (45, '李四', '111@qq.com', '你好张三', 0, 0, 68, 44, '7', '2020-01-20 22:39:28', '2020-03-21 22:27:06');
INSERT INTO `t_comment` VALUES (46, '张三', '111@qq.com', '你也好', 0, 0, 68, 45, '7', '2020-01-20 22:39:51', '2020-03-21 22:27:06');
INSERT INTO `t_comment` VALUES (47, '王五', '111@qq.com', '啊啊啊', 0, 0, 68, NULL, '7', '2020-01-20 22:41:07', '2020-03-21 22:27:06');
INSERT INTO `t_comment` VALUES (48, '66', '66', '66', 0, 0, 69, NULL, '8', '2020-01-31 15:51:12', '2020-03-21 22:27:06');
INSERT INTO `t_comment` VALUES (49, '请问', '1346138651@qq.com', 'q\'q\'we', 0, 0, 70, NULL, '9', '2020-02-08 15:02:49', '2020-03-21 22:27:06');
INSERT INTO `t_comment` VALUES (50, '张三', '1', '你好', 0, 0, 69, 48, '10', '2020-02-12 17:17:35', '2020-03-21 22:27:06');
INSERT INTO `t_comment` VALUES (51, '123', '123', '123456', 0, 0, 57, NULL, NULL, '2020-03-27 09:28:12', '2020-03-27 09:28:12');
INSERT INTO `t_comment` VALUES (54, '321', '123', '321', 0, 0, 57, NULL, NULL, '2020-03-27 09:32:43', '2020-03-27 09:32:43');
INSERT INTO `t_comment` VALUES (56, '77', '123', '123123', 0, 0, 76, NULL, '192.168.0.102', '2020-03-31 20:36:09', '2020-03-31 20:36:09');

-- ----------------------------
-- Table structure for t_friend
-- ----------------------------
DROP TABLE IF EXISTS `t_friend`;
CREATE TABLE `t_friend`  (
  `fr_id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '名称',
  `description` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '简介',
  `website` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '地址',
  `flag` int(2) NOT NULL DEFAULT 1 COMMENT '标识',
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  PRIMARY KEY (`fr_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_friend
-- ----------------------------
INSERT INTO `t_friend` VALUES (1, '纯洁的微笑', '大佬的博客，安利一波', 'http://www.ityouknow.com', 2, '2020-03-21 22:23:25', '2020-03-27 09:49:34');
INSERT INTO `t_friend` VALUES (2, '测试', '博客哈哈', 'https://www.baidu.com/?tn=18029102_oem_dg', 1, '2020-03-21 22:23:25', '2020-03-21 22:23:25');
INSERT INTO `t_friend` VALUES (3, '测试', '博客哈哈', 'https://www.baidu.com/?tn=18029102_oem_dg', 1, '2020-03-21 22:23:25', '2020-03-21 22:23:25');
INSERT INTO `t_friend` VALUES (4, '测试', '博客哈哈', 'https://www.baidu.com/?tn=18029102_oem_dg', 1, '2020-03-21 22:23:25', '2020-03-21 22:23:25');
INSERT INTO `t_friend` VALUES (5, '测试', '博客哈哈', 'https://www.baidu.com/?tn=18029102_oem_dg', 1, '2020-03-21 22:23:25', '2020-03-21 22:23:25');
INSERT INTO `t_friend` VALUES (6, '测试', '博客哈哈', 'https://www.baidu.com/?tn=18029102_oem_dg', 2, '2020-03-21 22:23:25', '2020-03-21 22:23:25');
INSERT INTO `t_friend` VALUES (7, '测试', '博客哈哈', 'https://www.baidu.com/?tn=18029102_oem_dg', 2, '2020-03-21 22:23:25', '2020-03-21 22:23:25');

-- ----------------------------
-- Table structure for t_request_log
-- ----------------------------
DROP TABLE IF EXISTS `t_request_log`;
CREATE TABLE `t_request_log`  (
  `log_id` bigint(11) NOT NULL AUTO_INCREMENT,
  `url` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '请求地址',
  `ip_address` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '访问者ip',
  `class_method` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '请求类名',
  `args` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '请求参数',
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  PRIMARY KEY (`log_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 228 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_request_log
-- ----------------------------
INSERT INTO `t_request_log` VALUES (218, '/peng/admin/blog/add', '127.0.0.1', 'com.peng.controller.Admin.BlogController.add', '[Blog(blId=null, title=1, content=<p>哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈', '2020-04-01 11:51:30');
INSERT INTO `t_request_log` VALUES (219, '/peng/admin/blog/add', '127.0.0.1', 'com.peng.controller.Admin.BlogController.add', '[Blog(blId=null, title=大大大撒as的阿斯顿撒旦阿三, content=<p>大大大撒as的阿斯顿撒旦阿三大大大撒as的阿斯顿撒旦阿三大大大撒as的阿斯顿撒大大大撒as的阿斯顿撒旦阿三顿撒旦阿三大大大撒as的阿斯顿撒旦阿三大大大撒as的阿斯顿撒旦阿三大大大撒as的阿斯顿撒旦阿三大大大撒as的阿斯顿撒旦阿三大大大撒as的阿斯顿撒旦阿三大大大撒as的阿斯顿撒旦阿三大大大撒as的阿斯顿撒旦阿三大大大撒as的阿斯顿撒旦阿三大大大撒as的阿斯顿撒旦阿三大大大撒as的阿斯顿撒......', '2020-04-01 11:56:10');
INSERT INTO `t_request_log` VALUES (220, '/peng/admin/blog/delete/86', '127.0.0.1', 'com.peng.controller.Admin.BlogController.removeById', '[86]', '2020-04-01 11:59:09');
INSERT INTO `t_request_log` VALUES (221, '/peng/admin/blog/delete/85', '127.0.0.1', 'com.peng.controller.Admin.BlogController.removeById', '[85]', '2020-04-01 11:59:38');
INSERT INTO `t_request_log` VALUES (222, '/peng/admin/blog/delete/84', '127.0.0.1', 'com.peng.controller.Admin.BlogController.removeById', '[84]', '2020-04-01 11:59:45');
INSERT INTO `t_request_log` VALUES (223, '/peng/admin/blog/delete/83', '127.0.0.1', 'com.peng.controller.Admin.BlogController.removeById', '[83]', '2020-04-01 11:59:47');
INSERT INTO `t_request_log` VALUES (224, '/peng/admin/blog/delete/82', '127.0.0.1', 'com.peng.controller.Admin.BlogController.removeById', '[82]', '2020-04-01 11:59:50');
INSERT INTO `t_request_log` VALUES (225, '/peng/admin/blog/delete/81', '127.0.0.1', 'com.peng.controller.Admin.BlogController.removeById', '[81]', '2020-04-01 11:59:52');
INSERT INTO `t_request_log` VALUES (226, '/peng/admin/blog/delete/80', '127.0.0.1', 'com.peng.controller.Admin.BlogController.removeById', '[80]', '2020-04-01 11:59:54');
INSERT INTO `t_request_log` VALUES (227, '/peng/admin/blog/delete/79', '127.0.0.1', 'com.peng.controller.Admin.BlogController.removeById', '[79]', '2020-04-01 11:59:57');

-- ----------------------------
-- Table structure for t_tag
-- ----------------------------
DROP TABLE IF EXISTS `t_tag`;
CREATE TABLE `t_tag`  (
  `ta_id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '标签名称',
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  PRIMARY KEY (`ta_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_tag
-- ----------------------------
INSERT INTO `t_tag` VALUES (1, '链表', '2020-03-21 22:14:52', '2020-03-21 22:14:52');
INSERT INTO `t_tag` VALUES (2, '递归', '2020-03-21 22:14:52', '2020-03-21 22:14:52');
INSERT INTO `t_tag` VALUES (3, '回溯', '2020-03-21 22:14:52', '2020-03-21 22:14:52');
INSERT INTO `t_tag` VALUES (4, '动态规划', '2020-03-21 22:14:52', '2020-03-21 22:14:52');
INSERT INTO `t_tag` VALUES (5, 'SQL', '2020-03-21 22:14:52', '2020-03-21 22:14:52');
INSERT INTO `t_tag` VALUES (6, 'AcitveMQ', '2020-03-21 22:14:52', '2020-03-21 22:14:52');
INSERT INTO `t_tag` VALUES (7, 'SpringBoot', '2020-03-21 22:14:52', '2020-03-31 14:34:04');

-- ----------------------------
-- Table structure for t_type
-- ----------------------------
DROP TABLE IF EXISTS `t_type`;
CREATE TABLE `t_type`  (
  `ty_id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '类型名',
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  PRIMARY KEY (`ty_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_type
-- ----------------------------
INSERT INTO `t_type` VALUES (1, 'JUC', '2020-03-21 22:11:52', '2020-03-21 22:11:52');
INSERT INTO `t_type` VALUES (2, '力扣', '2020-03-21 22:11:52', '2020-03-21 22:11:52');
INSERT INTO `t_type` VALUES (3, '算法', '2020-03-21 22:11:52', '2020-03-21 22:11:52');
INSERT INTO `t_type` VALUES (4, '数据库', '2020-03-21 22:11:52', '2020-03-21 22:11:52');
INSERT INTO `t_type` VALUES (5, 'JVM', '2020-03-21 22:11:52', '2020-03-21 22:11:52');
INSERT INTO `t_type` VALUES (6, 'Java', '2020-03-21 22:11:52', '2020-03-21 22:11:52');
INSERT INTO `t_type` VALUES (7, '中间件', '2020-03-21 22:11:52', '2020-03-21 22:11:52');

SET FOREIGN_KEY_CHECKS = 1;
